diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index b4563dd1f9a9..bcb477447584 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -63,6 +63,13 @@
 /src/plugins/apm_oss/  @elastic/apm-ui
 /src/apm.js @watson @vigneshshanmugam
 
+# Client Side Monitoring (lives in APM directories but owned by Uptime)
+/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum @elastic/uptime
+/x-pack/plugins/apm/public/application/csmApp.tsx @elastic/uptime
+/x-pack/plugins/apm/public/components/app/RumDashboard @elastic/uptime
+/x-pack/plugins/apm/server/lib/rum_client @elastic/uptime
+/x-pack/plugins/apm/server/routes/rum_client.ts @elastic/uptime
+
 # Beats
 /x-pack/legacy/plugins/beats_management/  @elastic/beats
 
diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc
index 9f13c152b4cb..a64a0330ae43 100644
--- a/docs/management/advanced-options.asciidoc
+++ b/docs/management/advanced-options.asciidoc
@@ -150,6 +150,12 @@ working on big documents.
 ==== Machine learning
 
 [horizontal]
+`ml:anomalyDetection:results:enableTimeDefaults`:: Use the default time filter
+in the *Single Metric Viewer* and *Anomaly Explorer*. If this setting is
+disabled, the results for the full time range are shown.
+`ml:anomalyDetection:results:timeDefaults`:: Sets the default time filter for
+viewing {anomaly-job} results. This setting must contain `from` and `to` values (see {ref}/common-options.html#date-math[accepted formats]). It is ignored
+unless `ml:anomalyDetection:results:enableTimeDefaults` is enabled.
 `ml:fileDataVisualizerMaxFileSize`:: Sets the file size limit when importing
 data in the {data-viz}. The default value is `100MB`. The highest supported
 value for this setting is `1GB`.
diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc
index 88858c36643e..13c1d20552fa 100644
--- a/docs/settings/alert-action-settings.asciidoc
+++ b/docs/settings/alert-action-settings.asciidoc
@@ -37,10 +37,10 @@ You can configure the following settings in the `kibana.yml` file.
 [cols="2*<"]
 |===
 
-| `xpack.actions.whitelistedHosts` {ess-icon}
-  | A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly whitelisted. An empty list `[]` can be used to block built-in actions from making any external connections. +
+| `xpack.actions.allowedHosts` {ess-icon}
+  | A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly added to the allowed hosts. An empty list `[]` can be used to block built-in actions from making any external connections. +
   +
-  Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically whitelisted. If you are not using the default `[*]` setting, you must ensure that the corresponding endpoints are whitelisted as well.
+  Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically added to allowed hosts. If you are not using the default `[*]` setting, you must ensure that the corresponding endpoints are added to the allowed hosts as well.
 
 | `xpack.actions.enabledActionTypes` {ess-icon}
   | A list of action types that are enabled. It defaults to `[*]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, and `.webhook`. An empty list `[]` will disable all action types. +
diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc
index f6a02b9038c0..83e7edc5a016 100644
--- a/docs/user/alerting/action-types/email.asciidoc
+++ b/docs/user/alerting/action-types/email.asciidoc
@@ -12,7 +12,7 @@ Email connectors have the following configuration properties:
 
 Name::      The name of the connector. The name is used to identify a  connector in the management UI connector listing, or in the connector list when configuring an action.
 Sender::    The from address for all emails sent with this connector, specified in `user@host-name` format.
-Host::      Host name of the service provider. If you are using the <<action-settings, `xpack.actions.whitelistedHosts`>> setting, make sure this hostname is whitelisted.
+Host::      Host name of the service provider. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure this hostname is added to the allowed hosts.
 Port::      The port to connect to on the service provider.
 Secure::    If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information.
 Username::  username for 'login' type authentication.
diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc
index 5fd85a104526..2c9add5233c9 100644
--- a/docs/user/alerting/action-types/pagerduty.asciidoc
+++ b/docs/user/alerting/action-types/pagerduty.asciidoc
@@ -132,7 +132,7 @@ This is an irreversible action and impacts all alerts that use this connector.
 PagerDuty connectors have the following configuration properties:
 
 Name::      The name of the connector. The name is used to identify a  connector in the management UI connector listing, or in the connector list when configuring an action.
-API URL::   An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <<action-settings, `xpack.actions.whitelistedHosts`>> setting, make sure the hostname is whitelisted.
+API URL::   An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure the hostname is added to the allowed hosts.
 Integration Key::   A 32 character PagerDuty Integration Key for an integration on a service, also referred to as the routing key.
 
 [float]
diff --git a/docs/user/alerting/action-types/slack.asciidoc b/docs/user/alerting/action-types/slack.asciidoc
index 99bf73c0f559..a1fe7a2521b2 100644
--- a/docs/user/alerting/action-types/slack.asciidoc
+++ b/docs/user/alerting/action-types/slack.asciidoc
@@ -11,7 +11,7 @@ The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incomin
 Slack connectors have the following configuration properties:
 
 Name::      The name of the connector. The name is used to identify a  connector in the management UI connector listing, or in the connector list when configuring an action.
-Webhook URL::   The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <<action-settings, `xpack.actions.whitelistedHosts`>> setting, make sure the hostname is whitelisted.
+Webhook URL::   The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure the hostname is added to the allowed hosts.
 
 [float]
 [[Preconfigured-slack-configuration]]
diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc
index c91c24430e98..659c3afad6bd 100644
--- a/docs/user/alerting/action-types/webhook.asciidoc
+++ b/docs/user/alerting/action-types/webhook.asciidoc
@@ -11,7 +11,7 @@ The Webhook action type uses https://github.com/axios/axios[axios] to send a POS
 Webhook connectors have the following configuration properties:
 
 Name::      The name of the connector. The name is used to identify a  connector in the management UI connector listing, or in the connector list when configuring an action.
-URL::       The request URL. If you are using the <<action-settings, `xpack.actions.whitelistedHosts`>> setting, make sure the hostname is whitelisted.
+URL::       The request URL. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure the hostname is added to the allowed hosts.
 Method::    HTTP request method, either `post`(default) or `put`.
 Headers::   A set of key-value pairs sent as headers with the request
 User::      An optional username. If set, HTTP basic authentication is used. Currently only basic authentication is supported.
diff --git a/docs/visualize/images/lens_drag_drop.gif b/docs/visualize/images/lens_drag_drop.gif
index ca62115e7ea3..1f8580d46270 100644
Binary files a/docs/visualize/images/lens_drag_drop.gif and b/docs/visualize/images/lens_drag_drop.gif differ
diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx
index b033fe86cd1c..48b81c27e8b8 100644
--- a/examples/embeddable_examples/public/book/book_embeddable.tsx
+++ b/examples/embeddable_examples/public/book/book_embeddable.tsx
@@ -71,11 +71,7 @@ export class BookEmbeddable
 
   constructor(
     initialInput: BookEmbeddableInput,
-    private attributeService: AttributeService<
-      BookSavedObjectAttributes,
-      BookByValueInput,
-      BookByReferenceInput
-    >,
+    private attributeService: AttributeService<BookSavedObjectAttributes>,
     {
       parent,
     }: {
@@ -99,18 +95,21 @@ export class BookEmbeddable
     });
   }
 
-  inputIsRefType = (input: BookEmbeddableInput): input is BookByReferenceInput => {
+  readonly inputIsRefType = (input: BookEmbeddableInput): input is BookByReferenceInput => {
     return this.attributeService.inputIsRefType(input);
   };
 
-  getInputAsValueType = async (): Promise<BookByValueInput> => {
+  readonly getInputAsValueType = async (): Promise<BookByValueInput> => {
     const input = this.attributeService.getExplicitInputFromEmbeddable(this);
     return this.attributeService.getInputAsValueType(input);
   };
 
-  getInputAsRefType = async (): Promise<BookByReferenceInput> => {
+  readonly getInputAsRefType = async (): Promise<BookByReferenceInput> => {
     const input = this.attributeService.getExplicitInputFromEmbeddable(this);
-    return this.attributeService.getInputAsRefType(input, { showSaveModal: true });
+    return this.attributeService.getInputAsRefType(input, {
+      showSaveModal: true,
+      saveModalTitle: this.getTitle(),
+    });
   };
 
   public render(node: HTMLElement) {
diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx
index 4c144c3843c4..292261ee16c5 100644
--- a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx
+++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx
@@ -31,8 +31,6 @@ import {
   BOOK_EMBEDDABLE,
   BookEmbeddableInput,
   BookEmbeddableOutput,
-  BookByValueInput,
-  BookByReferenceInput,
 } from './book_embeddable';
 import { CreateEditBookComponent } from './create_edit_book_component';
 import { OverlayStart } from '../../../../src/core/public';
@@ -66,11 +64,7 @@ export class BookEmbeddableFactoryDefinition
     getIconForSavedObject: () => 'pencil',
   };
 
-  private attributeService?: AttributeService<
-    BookSavedObjectAttributes,
-    BookByValueInput,
-    BookByReferenceInput
-  >;
+  private attributeService?: AttributeService<BookSavedObjectAttributes>;
 
   constructor(private getStartServices: () => Promise<StartServices>) {}
 
@@ -126,9 +120,7 @@ export class BookEmbeddableFactoryDefinition
   private async getAttributeService() {
     if (!this.attributeService) {
       this.attributeService = await (await this.getStartServices()).getAttributeService<
-        BookSavedObjectAttributes,
-        BookByValueInput,
-        BookByReferenceInput
+        BookSavedObjectAttributes
       >(this.type);
     }
     return this.attributeService!;
diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx
index 5b14dc85b1fc..3541ace1e5e7 100644
--- a/examples/embeddable_examples/public/book/edit_book_action.tsx
+++ b/examples/embeddable_examples/public/book/edit_book_action.tsx
@@ -57,13 +57,13 @@ export const createEditBookAction = (getStartServices: () => Promise<StartServic
     },
     execute: async ({ embeddable }: ActionContext) => {
       const { openModal, getAttributeService } = await getStartServices();
-      const attributeService = getAttributeService<
-        BookSavedObjectAttributes,
-        BookByValueInput,
-        BookByReferenceInput
-      >(BOOK_SAVED_OBJECT);
+      const attributeService = getAttributeService<BookSavedObjectAttributes>(BOOK_SAVED_OBJECT);
       const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => {
-        const newInput = await attributeService.wrapAttributes(attributes, useRefType, embeddable);
+        const newInput = await attributeService.wrapAttributes(
+          attributes,
+          useRefType,
+          attributeService.getExplicitInputFromEmbeddable(embeddable)
+        );
         if (!useRefType && (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId) {
           // Set the saved object ID to null so that update input will remove the existing savedObjectId...
           (newInput as BookByValueInput & { savedObjectId: unknown }).savedObjectId = null;
diff --git a/package.json b/package.json
index cbf8fd6bc3bd..ff487510f7a3 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,7 @@
     "**/istanbul-instrumenter-loader/schema-utils": "1.0.0",
     "**/image-diff/gm/debug": "^2.6.9",
     "**/load-grunt-config/lodash": "^4.17.20",
+    "**/node-jose/node-forge": "^0.10.0",
     "**/react-dom": "^16.12.0",
     "**/react": "^16.12.0",
     "**/react-test-renderer": "^16.12.0",
@@ -191,7 +192,7 @@
     "moment-timezone": "^0.5.27",
     "mustache": "2.3.2",
     "node-fetch": "1.7.3",
-    "node-forge": "^0.9.1",
+    "node-forge": "^0.10.0",
     "opn": "^5.5.0",
     "oppsy": "^2.0.0",
     "p-map": "^4.0.0",
@@ -305,7 +306,7 @@
     "@types/moment-timezone": "^0.5.12",
     "@types/mustache": "^0.8.31",
     "@types/node": ">=10.17.17 <10.20.0",
-    "@types/node-forge": "^0.9.0",
+    "@types/node-forge": "^0.9.5",
     "@types/normalize-path": "^3.0.0",
     "@types/opn": "^5.1.0",
     "@types/pegjs": "^0.10.1",
diff --git a/packages/elastic-eslint-config-kibana/typescript.js b/packages/elastic-eslint-config-kibana/typescript.js
index 18b11eb62bee..d3e80b744815 100644
--- a/packages/elastic-eslint-config-kibana/typescript.js
+++ b/packages/elastic-eslint-config-kibana/typescript.js
@@ -223,7 +223,8 @@ module.exports = {
           'no-undef-init': 'error',
           'no-unsafe-finally': 'error',
           'no-unsanitized/property': 'error',
-          'no-unused-expressions': 'error',
+          'no-unused-expressions': 'off',
+          '@typescript-eslint/no-unused-expressions': 'error',
           'no-unused-labels': 'error',
           'no-var': 'error',
           'object-shorthand': 'error',
diff --git a/src/core/server/legacy/config/index.ts b/src/core/server/legacy/config/index.ts
index f10e3f22d53c..b56b83ca324c 100644
--- a/src/core/server/legacy/config/index.ts
+++ b/src/core/server/legacy/config/index.ts
@@ -19,4 +19,3 @@
 
 export { ensureValidConfiguration } from './ensure_valid_configuration';
 export { LegacyObjectToConfigAdapter } from './legacy_object_to_config_adapter';
-export { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters';
diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts
deleted file mode 100644
index b09f9d00b3be..000000000000
--- a/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { ConfigDeprecation } from '../../config';
-import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory';
-import { applyDeprecations } from '../../config/deprecation/apply_deprecations';
-import { LegacyConfigDeprecationProvider } from '../types';
-import { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters';
-
-jest.spyOn(configDeprecationFactory, 'unusedFromRoot');
-jest.spyOn(configDeprecationFactory, 'renameFromRoot');
-
-const executeHandlers = (handlers: ConfigDeprecation[]) => {
-  handlers.forEach((handler) => {
-    handler({}, '', () => null);
-  });
-};
-
-describe('convertLegacyDeprecationProvider', () => {
-  beforeEach(() => {
-    jest.clearAllMocks();
-  });
-
-  it('returns the same number of handlers', async () => {
-    const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [
-      rename('a', 'b'),
-      unused('c'),
-      unused('d'),
-    ];
-
-    const migrated = await convertLegacyDeprecationProvider(legacyProvider);
-    const handlers = migrated(configDeprecationFactory);
-    expect(handlers).toHaveLength(3);
-  });
-
-  it('invokes the factory "unusedFromRoot" when using legacy "unused"', async () => {
-    const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [
-      rename('a', 'b'),
-      unused('c'),
-      unused('d'),
-    ];
-
-    const migrated = await convertLegacyDeprecationProvider(legacyProvider);
-    const handlers = migrated(configDeprecationFactory);
-    executeHandlers(handlers);
-
-    expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledTimes(2);
-    expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledWith('c');
-    expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledWith('d');
-  });
-
-  it('invokes the factory "renameFromRoot" when using legacy "rename"', async () => {
-    const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [
-      rename('a', 'b'),
-      unused('c'),
-      rename('d', 'e'),
-    ];
-
-    const migrated = await convertLegacyDeprecationProvider(legacyProvider);
-    const handlers = migrated(configDeprecationFactory);
-    executeHandlers(handlers);
-
-    expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledTimes(2);
-    expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledWith('a', 'b');
-    expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledWith('d', 'e');
-  });
-
-  it('properly works in a real use case', async () => {
-    const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [
-      rename('old', 'new'),
-      unused('unused'),
-      unused('notpresent'),
-    ];
-
-    const convertedProvider = await convertLegacyDeprecationProvider(legacyProvider);
-    const handlers = convertedProvider(configDeprecationFactory);
-
-    const rawConfig = {
-      old: 'oldvalue',
-      unused: 'unused',
-      goodValue: 'good',
-    };
-
-    const migrated = applyDeprecations(
-      rawConfig,
-      handlers.map((handler) => ({ deprecation: handler, path: '' }))
-    );
-    expect(migrated).toEqual({ new: 'oldvalue', goodValue: 'good' });
-  });
-});
diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.ts
deleted file mode 100644
index 1e0733969e66..000000000000
--- a/src/core/server/legacy/config/legacy_deprecation_adapters.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { ConfigDeprecation, ConfigDeprecationProvider } from '../../config/deprecation';
-import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory';
-import { LegacyConfigDeprecation, LegacyConfigDeprecationProvider } from '../types';
-
-const convertLegacyDeprecation = (
-  legacyDeprecation: LegacyConfigDeprecation
-): ConfigDeprecation => (config, fromPath, logger) => {
-  legacyDeprecation(config, logger);
-  return config;
-};
-
-const legacyUnused = (unusedKey: string): LegacyConfigDeprecation => (settings, log) => {
-  const deprecation = configDeprecationFactory.unusedFromRoot(unusedKey);
-  deprecation(settings, '', log);
-};
-
-const legacyRename = (oldKey: string, newKey: string): LegacyConfigDeprecation => (
-  settings,
-  log
-) => {
-  const deprecation = configDeprecationFactory.renameFromRoot(oldKey, newKey);
-  deprecation(settings, '', log);
-};
-
-/**
- * Async deprecation provider converter for legacy deprecation implementation
- *
- * @internal
- */
-export const convertLegacyDeprecationProvider = async (
-  legacyProvider: LegacyConfigDeprecationProvider
-): Promise<ConfigDeprecationProvider> => {
-  const legacyDeprecations = await legacyProvider({
-    rename: legacyRename,
-    unused: legacyUnused,
-  });
-  return () => legacyDeprecations.map(convertLegacyDeprecation);
-};
diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts
index 45869fd12d2b..d0492ea88c5e 100644
--- a/src/core/server/legacy/legacy_service.test.ts
+++ b/src/core/server/legacy/legacy_service.test.ts
@@ -19,9 +19,7 @@
 
 jest.mock('../../../legacy/server/kbn_server');
 jest.mock('../../../cli/cluster/cluster_manager');
-jest.mock('./config/legacy_deprecation_adapters', () => ({
-  convertLegacyDeprecationProvider: (provider: any) => Promise.resolve(provider),
-}));
+
 import {
   findLegacyPluginSpecsMock,
   logLegacyThirdPartyPluginDeprecationWarningMock,
@@ -446,46 +444,8 @@ describe('#discoverPlugins()', () => {
     expect(findLegacyPluginSpecs).toHaveBeenCalledWith(expect.any(Object), logger, env.packageInfo);
   });
 
-  it(`register legacy plugin's deprecation providers`, async () => {
-    findLegacyPluginSpecsMock.mockImplementation(
-      (settings) =>
-        Promise.resolve({
-          pluginSpecs: [
-            {
-              getDeprecationsProvider: () => undefined,
-            },
-            {
-              getDeprecationsProvider: () => 'providerA',
-            },
-            {
-              getDeprecationsProvider: () => 'providerB',
-            },
-          ],
-          pluginExtendedConfig: settings,
-          disabledPluginSpecs: [],
-          uiExports: {},
-          navLinks: [],
-        }) as any
-    );
-
-    const legacyService = new LegacyService({
-      coreId,
-      env,
-      logger,
-      configService: configService as any,
-    });
-
-    await legacyService.discoverPlugins();
-    expect(configService.addDeprecationProvider).toHaveBeenCalledTimes(2);
-    expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerA');
-    expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerB');
-  });
-
   it(`logs deprecations for legacy third party plugins`, async () => {
-    const pluginSpecs = [
-      { getId: () => 'pluginA', getDeprecationsProvider: () => undefined },
-      { getId: () => 'pluginB', getDeprecationsProvider: () => undefined },
-    ];
+    const pluginSpecs = [{ getId: () => 'pluginA' }, { getId: () => 'pluginB' }];
     findLegacyPluginSpecsMock.mockImplementation(
       (settings) =>
         Promise.resolve({
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index adfdecdd7c97..880011d2e192 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -21,7 +21,7 @@ import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription }
 import { first, map, publishReplay, tap } from 'rxjs/operators';
 
 import { CoreService } from '../../types';
-import { Config, ConfigDeprecationProvider } from '../config';
+import { Config } from '../config';
 import { CoreContext } from '../core_context';
 import { CspConfigType, config as cspConfig } from '../csp';
 import { DevConfig, DevConfigType, config as devConfig } from '../dev';
@@ -29,7 +29,6 @@ import { BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig }
 import { Logger } from '../logging';
 import { PathConfigType } from '../path';
 import { findLegacyPluginSpecs, logLegacyThirdPartyPluginDeprecationWarning } from './plugins';
-import { convertLegacyDeprecationProvider } from './config';
 import {
   ILegacyInternals,
   LegacyServiceSetupDeps,
@@ -145,18 +144,6 @@ export class LegacyService implements CoreService {
       navLinks,
     };
 
-    const deprecationProviders = await pluginSpecs
-      .map((spec) => spec.getDeprecationsProvider())
-      .reduce(async (providers, current) => {
-        if (current) {
-          return [...(await providers), await convertLegacyDeprecationProvider(current)];
-        }
-        return providers;
-      }, Promise.resolve([] as ConfigDeprecationProvider[]));
-    deprecationProviders.forEach((provider) =>
-      this.coreContext.configService.addDeprecationProvider('', provider)
-    );
-
     this.legacyRawConfig = pluginExtendedConfig;
 
     // check for unknown uiExport types
diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts
index dfa2396d5904..2317f1036ce4 100644
--- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts
+++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts
@@ -26,7 +26,6 @@ const createPluginSpec = ({ id, path }: { id: string; path: string }): LegacyPlu
     getId: () => id,
     getExpectedKibanaVersion: () => 'kibana',
     getConfigPrefix: () => 'plugin.config',
-    getDeprecationsProvider: () => undefined,
     getPack: () => ({
       getPath: () => path,
     }),
diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts
index 98f8d874c708..cf08689a6d0d 100644
--- a/src/core/server/legacy/types.ts
+++ b/src/core/server/legacy/types.ts
@@ -51,36 +51,6 @@ export interface LegacyConfig {
   set(config: LegacyVars): void;
 }
 
-/**
- * Representation of a legacy configuration deprecation factory used for
- * legacy plugin deprecations.
- *
- * @internal
- * @deprecated
- */
-export interface LegacyConfigDeprecationFactory {
-  rename(oldKey: string, newKey: string): LegacyConfigDeprecation;
-  unused(unusedKey: string): LegacyConfigDeprecation;
-}
-
-/**
- * Representation of a legacy configuration deprecation.
- *
- * @internal
- * @deprecated
- */
-export type LegacyConfigDeprecation = (settings: LegacyVars, log: (msg: string) => void) => void;
-
-/**
- * Representation of a legacy configuration deprecation provider.
- *
- * @internal
- * @deprecated
- */
-export type LegacyConfigDeprecationProvider = (
-  factory: LegacyConfigDeprecationFactory
-) => LegacyConfigDeprecation[] | Promise<LegacyConfigDeprecation[]>;
-
 /**
  * @internal
  * @deprecated
@@ -97,7 +67,6 @@ export interface LegacyPluginSpec {
   getId: () => unknown;
   getExpectedKibanaVersion: () => string;
   getConfigPrefix: () => string;
-  getDeprecationsProvider: () => LegacyConfigDeprecationProvider | undefined;
   getPack: () => LegacyPluginPack;
 }
 
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 1123433e30ac..3270e5a09afd 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2949,11 +2949,11 @@ export const validBodyOutput: readonly ["data", "stream"];
 // Warnings were encountered during analysis:
 //
 // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:163:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:164:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:166:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts
-// src/core/server/legacy/types.ts:167:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:132:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:133:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:134:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:135:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts
+// src/core/server/legacy/types.ts:136:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts
 // src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
 // src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
 // src/core/server/plugins/types.ts:268:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts
diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json
deleted file mode 100644
index 1a9e6253bff7..000000000000
--- a/src/core/tsconfig.json
+++ /dev/null
@@ -1,23 +0,0 @@
-// {
-//   "extends": "../../tsconfig.base.json",
-//   "compilerOptions": {
-// //    "composite": true,
-//     "outDir": "./target",
-//     "emitDeclarationOnly": true,
-//     "declaration": true,
-//     "declarationMap": true
-//   },
-//   "include": [
-//     "public",
-//     "server",
-//     "types",
-//     "test_helpers",
-//     "utils",
-//     "index.ts",
-//     "../../kibana.d.ts",
-//     "../../typings/**/*"
-//   ],
-//   "references": [
-//     { "path": "../test_utils" }
-//   ]
-// }
diff --git a/src/dev/build/tasks/create_archives_task.ts b/src/dev/build/tasks/create_archives_task.ts
index 0083881e9f74..a05e383394ec 100644
--- a/src/dev/build/tasks/create_archives_task.ts
+++ b/src/dev/build/tasks/create_archives_task.ts
@@ -92,8 +92,8 @@ export const CreateArchives: Task = {
       });
 
       metrics.push({
-        group: `${build.isOss() ? 'oss ' : ''}distributable file count`,
-        id: 'total',
+        group: 'distributable file count',
+        id: build.isOss() ? 'oss' : 'default',
         value: fileCount,
       });
     }
diff --git a/src/legacy/server/logging/log_format.js b/src/legacy/server/logging/log_format.js
index 8a80cbef1a9c..6edda8c4be90 100644
--- a/src/legacy/server/logging/log_format.js
+++ b/src/legacy/server/logging/log_format.js
@@ -91,7 +91,7 @@ export default class TransformObjStream extends Stream.Transform {
         method: event.method || '',
         headers: event.headers,
         remoteAddress: source.remoteAddress,
-        userAgent: source.remoteAddress,
+        userAgent: source.userAgent,
         referer: source.referer,
       };
 
diff --git a/src/legacy/server/logging/log_format_json.test.js b/src/legacy/server/logging/log_format_json.test.js
index f4fb93975056..ec7296d21672 100644
--- a/src/legacy/server/logging/log_format_json.test.js
+++ b/src/legacy/server/logging/log_format_json.test.js
@@ -65,12 +65,14 @@ describe('KbnLoggerJsonFormat', () => {
         },
       };
       const result = await createPromiseFromStreams([createListStream([event]), format]);
-      const { type, method, statusCode, message } = JSON.parse(result);
+      const { type, method, statusCode, message, req } = JSON.parse(result);
 
       expect(type).toBe('response');
       expect(method).toBe('GET');
       expect(statusCode).toBe(200);
       expect(message).toBe('GET /path/to/resource 200 12000ms - 13.0B');
+      expect(req.remoteAddress).toBe('127.0.0.1');
+      expect(req.userAgent).toBe('Test Thing');
     });
 
     it('ops', async () => {
diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
index 0d20fdee07df..212b54be9ae0 100644
--- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
@@ -24,9 +24,18 @@ import { EuiCheckboxGroupIdToSelectedMap } from '@elastic/eui/src/components/for
 import React, { useState, ReactElement } from 'react';
 import ReactDOM from 'react-dom';
 import angular from 'angular';
+import deepEqual from 'fast-deep-equal';
 
 import { Observable, pipe, Subscription, merge } from 'rxjs';
-import { filter, map, debounceTime, mapTo, startWith, switchMap } from 'rxjs/operators';
+import {
+  filter,
+  map,
+  debounceTime,
+  mapTo,
+  startWith,
+  switchMap,
+  distinctUntilChanged,
+} from 'rxjs/operators';
 import { History } from 'history';
 import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public';
 import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
@@ -279,6 +288,12 @@ export class DashboardAppController {
     const updateIndexPatternsOperator = pipe(
       filter((container: DashboardContainer) => !!container && !isErrorEmbeddable(container)),
       map(getDashboardIndexPatterns),
+      distinctUntilChanged((a, b) =>
+        deepEqual(
+          a.map((ip) => ip.id),
+          b.map((ip) => ip.id)
+        )
+      ),
       // using switchMap for previous task cancellation
       switchMap((panelIndexPatterns: IndexPattern[]) => {
         return new Observable((observer) => {
@@ -405,17 +420,29 @@ export class DashboardAppController {
               ) : null;
             };
 
-            outputSubscription = new Subscription();
-            outputSubscription.add(
-              dashboardContainer
-                .getOutput$()
-                .pipe(
-                  mapTo(dashboardContainer),
-                  startWith(dashboardContainer), // to trigger initial index pattern update
-                  updateIndexPatternsOperator
+            outputSubscription = merge(
+              // output of dashboard container itself
+              dashboardContainer.getOutput$(),
+              // plus output of dashboard container children,
+              // children may change, so make sure we subscribe/unsubscribe with switchMap
+              dashboardContainer.getOutput$().pipe(
+                map(() => dashboardContainer!.getChildIds()),
+                distinctUntilChanged(deepEqual),
+                switchMap((newChildIds: string[]) =>
+                  merge(
+                    ...newChildIds.map((childId) =>
+                      dashboardContainer!.getChild(childId).getOutput$()
+                    )
+                  )
                 )
-                .subscribe()
-            );
+              )
+            )
+              .pipe(
+                mapTo(dashboardContainer),
+                startWith(dashboardContainer), // to trigger initial index pattern update
+                updateIndexPatternsOperator
+              )
+              .subscribe();
 
             inputSubscription = dashboardContainer.getInput$().subscribe(() => {
               let dirty = false;
diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts b/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts
new file mode 100644
index 000000000000..06f380ca3862
--- /dev/null
+++ b/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts
@@ -0,0 +1,193 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ATTRIBUTE_SERVICE_KEY } from './attribute_service';
+import { mockAttributeService } from './attribute_service_mock';
+import { coreMock } from '../../../../core/public/mocks';
+
+interface TestAttributes {
+  title: string;
+  testAttr1?: string;
+  testAttr2?: { array: unknown[]; testAttr3: string };
+}
+
+interface TestByValueInput {
+  id: string;
+  [ATTRIBUTE_SERVICE_KEY]: TestAttributes;
+}
+
+describe('attributeService', () => {
+  const defaultTestType = 'defaultTestType';
+  let attributes: TestAttributes;
+  let byValueInput: TestByValueInput;
+  let byReferenceInput: { id: string; savedObjectId: string };
+
+  beforeEach(() => {
+    attributes = {
+      title: 'ultra title',
+      testAttr1: 'neat first attribute',
+      testAttr2: { array: [1, 2, 3], testAttr3: 'super attribute' },
+    };
+    byValueInput = {
+      id: '456',
+      attributes,
+    };
+    byReferenceInput = {
+      id: '456',
+      savedObjectId: '123',
+    };
+  });
+
+  describe('determining input type', () => {
+    const defaultAttributeService = mockAttributeService<TestAttributes>(defaultTestType);
+    const customAttributeService = mockAttributeService<TestAttributes, TestByValueInput>(
+      defaultTestType
+    );
+
+    it('can determine input type given default types', () => {
+      expect(
+        defaultAttributeService.inputIsRefType({ id: '456', savedObjectId: '123' })
+      ).toBeTruthy();
+      expect(
+        defaultAttributeService.inputIsRefType({
+          id: '456',
+          attributes: { title: 'wow I am by value' },
+        })
+      ).toBeFalsy();
+    });
+    it('can determine input type given custom types', () => {
+      expect(
+        customAttributeService.inputIsRefType({ id: '456', savedObjectId: '123' })
+      ).toBeTruthy();
+      expect(
+        customAttributeService.inputIsRefType({
+          id: '456',
+          [ATTRIBUTE_SERVICE_KEY]: { title: 'wow I am by value' },
+        })
+      ).toBeFalsy();
+    });
+  });
+
+  describe('unwrapping attributes', () => {
+    it('can unwrap all default attributes when given reference type input', async () => {
+      const core = coreMock.createStart();
+      core.savedObjects.client.get = jest.fn().mockResolvedValueOnce({
+        attributes,
+      });
+      const attributeService = mockAttributeService<TestAttributes>(
+        defaultTestType,
+        undefined,
+        core
+      );
+      expect(await attributeService.unwrapAttributes(byReferenceInput)).toEqual(attributes);
+    });
+
+    it('returns attributes when when given value type input', async () => {
+      const attributeService = mockAttributeService<TestAttributes>(defaultTestType);
+      expect(await attributeService.unwrapAttributes(byValueInput)).toEqual(attributes);
+    });
+
+    it('runs attributes through a custom unwrap method', async () => {
+      const core = coreMock.createStart();
+      core.savedObjects.client.get = jest.fn().mockResolvedValueOnce({
+        attributes,
+      });
+      const attributeService = mockAttributeService<TestAttributes>(
+        defaultTestType,
+        {
+          customUnwrapMethod: (savedObject) => ({
+            ...savedObject.attributes,
+            testAttr2: { array: [1, 2, 3, 4, 5], testAttr3: 'kibanana' },
+          }),
+        },
+        core
+      );
+      expect(await attributeService.unwrapAttributes(byReferenceInput)).toEqual({
+        ...attributes,
+        testAttr2: { array: [1, 2, 3, 4, 5], testAttr3: 'kibanana' },
+      });
+    });
+  });
+
+  describe('wrapping attributes', () => {
+    it('returns given attributes when use ref type is false', async () => {
+      const attributeService = mockAttributeService<TestAttributes>(defaultTestType);
+      expect(await attributeService.wrapAttributes(attributes, false)).toEqual({ attributes });
+    });
+
+    it('updates existing saved object with new attributes when given id', async () => {
+      const core = coreMock.createStart();
+      const attributeService = mockAttributeService<TestAttributes>(
+        defaultTestType,
+        undefined,
+        core
+      );
+      expect(await attributeService.wrapAttributes(attributes, true, byReferenceInput)).toEqual(
+        byReferenceInput
+      );
+      expect(core.savedObjects.client.update).toHaveBeenCalledWith(
+        defaultTestType,
+        '123',
+        attributes
+      );
+    });
+
+    it('creates new saved object with attributes when given no id', async () => {
+      const core = coreMock.createStart();
+      core.savedObjects.client.create = jest.fn().mockResolvedValueOnce({
+        id: '678',
+      });
+      const attributeService = mockAttributeService<TestAttributes>(
+        defaultTestType,
+        undefined,
+        core
+      );
+      expect(await attributeService.wrapAttributes(attributes, true)).toEqual({
+        savedObjectId: '678',
+      });
+      expect(core.savedObjects.client.create).toHaveBeenCalledWith(defaultTestType, attributes);
+    });
+
+    it('uses custom save method when given an id', async () => {
+      const customSaveMethod = jest.fn().mockReturnValue({ id: '123' });
+      const attributeService = mockAttributeService<TestAttributes>(defaultTestType, {
+        customSaveMethod,
+      });
+      expect(await attributeService.wrapAttributes(attributes, true, byReferenceInput)).toEqual(
+        byReferenceInput
+      );
+      expect(customSaveMethod).toHaveBeenCalledWith(
+        defaultTestType,
+        attributes,
+        byReferenceInput.savedObjectId
+      );
+    });
+
+    it('uses custom save method given no id', async () => {
+      const customSaveMethod = jest.fn().mockReturnValue({ id: '678' });
+      const attributeService = mockAttributeService<TestAttributes>(defaultTestType, {
+        customSaveMethod,
+      });
+      expect(await attributeService.wrapAttributes(attributes, true)).toEqual({
+        savedObjectId: '678',
+      });
+      expect(customSaveMethod).toHaveBeenCalledWith(defaultTestType, attributes, undefined);
+    });
+  });
+});
diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx
index fe5f6a0c8e2b..a36363d22d87 100644
--- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx
+++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx
@@ -19,11 +19,16 @@
 
 import React from 'react';
 import { i18n } from '@kbn/i18n';
+import { get } from 'lodash';
 import {
   EmbeddableInput,
   SavedObjectEmbeddableInput,
   isSavedObjectEmbeddableInput,
   IEmbeddable,
+  Container,
+  EmbeddableStart,
+  EmbeddableFactory,
+  EmbeddableFactoryNotFoundError,
 } from '../embeddable_plugin';
 import {
   SavedObjectsClientContract,
@@ -34,17 +39,10 @@ import {
 } from '../../../../core/public';
 import {
   SavedObjectSaveModal,
-  showSaveModal,
   OnSaveProps,
   SaveResult,
   checkForDuplicateTitle,
 } from '../../../saved_objects/public';
-import {
-  EmbeddableStart,
-  EmbeddableFactory,
-  EmbeddableFactoryNotFoundError,
-  Container,
-} from '../../../embeddable/public';
 
 /**
  * The attribute service is a shared, generic service that embeddables can use to provide the functionality
@@ -52,26 +50,46 @@ import {
  * can also be used as a higher level wrapper to transform an embeddable input shape that references a saved object
  * into an embeddable input shape that contains that saved object's attributes by value.
  */
+export const ATTRIBUTE_SERVICE_KEY = 'attributes';
+
+export interface AttributeServiceOptions<A extends { title: string }> {
+  customSaveMethod?: (
+    type: string,
+    attributes: A,
+    savedObjectId?: string
+  ) => Promise<{ id: string }>;
+  customUnwrapMethod?: (savedObject: SimpleSavedObject<A>) => A;
+}
+
 export class AttributeService<
   SavedObjectAttributes extends { title: string },
-  ValType extends EmbeddableInput & { attributes: SavedObjectAttributes },
-  RefType extends SavedObjectEmbeddableInput
+  ValType extends EmbeddableInput & {
+    [ATTRIBUTE_SERVICE_KEY]: SavedObjectAttributes;
+  } = EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: SavedObjectAttributes },
+  RefType extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
 > {
-  private embeddableFactory: EmbeddableFactory;
+  private embeddableFactory?: EmbeddableFactory;
 
   constructor(
     private type: string,
+    private showSaveModal: (
+      saveModal: React.ReactElement,
+      I18nContext: I18nStart['Context']
+    ) => void,
     private savedObjectsClient: SavedObjectsClientContract,
     private overlays: OverlayStart,
     private i18nContext: I18nStart['Context'],
     private toasts: NotificationsStart['toasts'],
-    getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']
+    getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'],
+    private options?: AttributeServiceOptions<SavedObjectAttributes>
   ) {
-    const factory = getEmbeddableFactory(this.type);
-    if (!factory) {
-      throw new EmbeddableFactoryNotFoundError(this.type);
+    if (getEmbeddableFactory) {
+      const factory = getEmbeddableFactory(this.type);
+      if (!factory) {
+        throw new EmbeddableFactoryNotFoundError(this.type);
+      }
+      this.embeddableFactory = factory;
     }
-    this.embeddableFactory = factory;
   }
 
   public async unwrapAttributes(input: RefType | ValType): Promise<SavedObjectAttributes> {
@@ -79,43 +97,54 @@ export class AttributeService<
       const savedObject: SimpleSavedObject<SavedObjectAttributes> = await this.savedObjectsClient.get<
         SavedObjectAttributes
       >(this.type, input.savedObjectId);
-      return savedObject.attributes;
+      return this.options?.customUnwrapMethod
+        ? this.options?.customUnwrapMethod(savedObject)
+        : { ...savedObject.attributes };
     }
-    return input.attributes;
+    return input[ATTRIBUTE_SERVICE_KEY];
   }
 
   public async wrapAttributes(
     newAttributes: SavedObjectAttributes,
     useRefType: boolean,
-    embeddable?: IEmbeddable
+    input?: ValType | RefType
   ): Promise<Omit<ValType | RefType, 'id'>> {
+    const originalInput = input ? input : {};
     const savedObjectId =
-      embeddable && isSavedObjectEmbeddableInput(embeddable.getInput())
-        ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId
+      input && this.inputIsRefType(input)
+        ? (input as SavedObjectEmbeddableInput).savedObjectId
         : undefined;
     if (!useRefType) {
-      return { attributes: newAttributes } as ValType;
-    } else {
-      try {
-        if (savedObjectId) {
-          await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes);
-          return { savedObjectId } as RefType;
-        } else {
-          const savedItem = await this.savedObjectsClient.create(this.type, newAttributes);
-          return { savedObjectId: savedItem.id } as RefType;
-        }
-      } catch (error) {
-        this.toasts.addDanger({
-          title: i18n.translate('dashboard.attributeService.saveToLibraryError', {
-            defaultMessage: `Panel was not saved to the library. Error: {errorMessage}`,
-            values: {
-              errorMessage: error.message,
-            },
-          }),
-          'data-test-subj': 'saveDashboardFailure',
-        });
-        return Promise.reject({ error });
+      return { [ATTRIBUTE_SERVICE_KEY]: newAttributes } as ValType;
+    }
+    try {
+      if (this.options?.customSaveMethod) {
+        const savedItem = await this.options.customSaveMethod(
+          this.type,
+          newAttributes,
+          savedObjectId
+        );
+        return { ...originalInput, savedObjectId: savedItem.id } as RefType;
+      }
+
+      if (savedObjectId) {
+        await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes);
+        return { ...originalInput, savedObjectId } as RefType;
       }
+
+      const savedItem = await this.savedObjectsClient.create(this.type, newAttributes);
+      return { ...originalInput, savedObjectId: savedItem.id } as RefType;
+    } catch (error) {
+      this.toasts.addDanger({
+        title: i18n.translate('dashboard.attributeService.saveToLibraryError', {
+          defaultMessage: `Panel was not saved to the library. Error: {errorMessage}`,
+          values: {
+            errorMessage: error.message,
+          },
+        }),
+        'data-test-subj': 'saveDashboardFailure',
+      });
+      return Promise.reject({ error });
     }
   }
 
@@ -146,7 +175,7 @@ export class AttributeService<
 
   getInputAsRefType = async (
     input: ValType | RefType,
-    saveOptions?: { showSaveModal: boolean } | { title: string }
+    saveOptions?: { showSaveModal: boolean; saveModalTitle?: string } | { title: string }
   ): Promise<RefType> => {
     if (this.inputIsRefType(input)) {
       return input;
@@ -159,7 +188,7 @@ export class AttributeService<
             copyOnSave: false,
             lastSavedTitle: '',
             getEsType: () => this.type,
-            getDisplayName: this.embeddableFactory.getDisplayName,
+            getDisplayName: this.embeddableFactory?.getDisplayName || (() => this.type),
           },
           props.isTitleDuplicateConfirmed,
           props.onTitleDuplicate,
@@ -169,7 +198,7 @@ export class AttributeService<
           }
         );
         try {
-          const newAttributes = { ...input.attributes };
+          const newAttributes = { ...input[ATTRIBUTE_SERVICE_KEY] };
           newAttributes.title = props.newTitle;
           const wrappedInput = (await this.wrapAttributes(newAttributes, true)) as RefType;
           resolve(wrappedInput);
@@ -181,11 +210,11 @@ export class AttributeService<
       };
 
       if (saveOptions && (saveOptions as { showSaveModal: boolean }).showSaveModal) {
-        showSaveModal(
+        this.showSaveModal(
           <SavedObjectSaveModal
             onSave={onSave}
             onClose={() => reject()}
-            title={input.attributes.title}
+            title={get(saveOptions, 'saveModalTitle', input[ATTRIBUTE_SERVICE_KEY].title)}
             showCopyOnSave={false}
             objectType={this.type}
             showDescription={false}
diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service_mock.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service_mock.tsx
new file mode 100644
index 000000000000..321a53361fc7
--- /dev/null
+++ b/src/plugins/dashboard/public/attribute_service/attribute_service_mock.tsx
@@ -0,0 +1,49 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { EmbeddableInput, SavedObjectEmbeddableInput } from '../embeddable_plugin';
+import { coreMock } from '../../../../core/public/mocks';
+import { AttributeServiceOptions } from './attribute_service';
+import { CoreStart } from '../../../../core/public';
+import { AttributeService, ATTRIBUTE_SERVICE_KEY } from '..';
+
+export const mockAttributeService = <
+  A extends { title: string },
+  V extends EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A } = EmbeddableInput & {
+    [ATTRIBUTE_SERVICE_KEY]: A;
+  },
+  R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
+>(
+  type: string,
+  options?: AttributeServiceOptions<A>,
+  customCore?: jest.Mocked<CoreStart>
+): AttributeService<A, V, R> => {
+  const core = customCore ? customCore : coreMock.createStart();
+  const service = new AttributeService<A, V, R>(
+    type,
+    jest.fn(),
+    core.savedObjects.client,
+    core.overlays,
+    core.i18n.Context,
+    core.notifications.toasts,
+    jest.fn().mockReturnValue(() => ({ getDisplayName: () => type })),
+    options
+  );
+  return service;
+};
diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts
index 8a9954cc77a2..e22d1f038a45 100644
--- a/src/plugins/dashboard/public/index.ts
+++ b/src/plugins/dashboard/public/index.ts
@@ -40,7 +40,7 @@ export {
 export { addEmbeddableToDashboardUrl } from './url_utils/url_helper';
 export { SavedObjectDashboard } from './saved_dashboards';
 export { SavedDashboardPanel } from './types';
-export { AttributeService } from './attribute_service/attribute_service';
+export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './attribute_service/attribute_service';
 
 export function plugin(initializerContext: PluginInitializerContext) {
   return new DashboardPlugin(initializerContext);
diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx
index 3df52f4e7a20..0ce6f9489ea0 100644
--- a/src/plugins/dashboard/public/plugin.tsx
+++ b/src/plugins/dashboard/public/plugin.tsx
@@ -52,6 +52,7 @@ import {
   getSavedObjectFinder,
   SavedObjectLoader,
   SavedObjectsStart,
+  showSaveModal,
 } from '../../saved_objects/public';
 import {
   ExitFullScreenButton as ExitFullScreenButtonUi,
@@ -102,6 +103,10 @@ import { addEmbeddableToDashboardUrl } from './url_utils/url_helper';
 import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder';
 import { UrlGeneratorState } from '../../share/public';
 import { AttributeService } from '.';
+import {
+  AttributeServiceOptions,
+  ATTRIBUTE_SERVICE_KEY,
+} from './attribute_service/attribute_service';
 
 declare module '../../share/public' {
   export interface UrlGeneratorStateMapping {
@@ -150,10 +155,13 @@ export interface DashboardStart {
   DashboardContainerByValueRenderer: ReturnType<typeof createDashboardContainerByValueRenderer>;
   getAttributeService: <
     A extends { title: string },
-    V extends EmbeddableInput & { attributes: A },
-    R extends SavedObjectEmbeddableInput
+    V extends EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A } = EmbeddableInput & {
+      [ATTRIBUTE_SERVICE_KEY]: A;
+    },
+    R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
   >(
-    type: string
+    type: string,
+    options?: AttributeServiceOptions<A>
   ) => AttributeService<A, V, R>;
 }
 
@@ -465,14 +473,16 @@ export class DashboardPlugin
       DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({
         factory: dashboardContainerFactory,
       }),
-      getAttributeService: (type: string) =>
+      getAttributeService: (type: string, options) =>
         new AttributeService(
           type,
+          showSaveModal,
           core.savedObjects.client,
           core.overlays,
           core.i18n.Context,
           core.notifications.toasts,
-          embeddable.getEmbeddableFactory
+          embeddable.getEmbeddableFactory,
+          options
         ),
     };
   }
diff --git a/src/plugins/ui_actions/public/triggers/select_range_trigger.ts b/src/plugins/ui_actions/public/triggers/select_range_trigger.ts
index f6d5547f6248..312e75314bd9 100644
--- a/src/plugins/ui_actions/public/triggers/select_range_trigger.ts
+++ b/src/plugins/ui_actions/public/triggers/select_range_trigger.ts
@@ -27,6 +27,6 @@ export const selectRangeTrigger: Trigger<'SELECT_RANGE_TRIGGER'> = {
     defaultMessage: 'Range selection',
   }),
   description: i18n.translate('uiActions.triggers.selectRangeDescription', {
-    defaultMessage: 'Select a group of values',
+    defaultMessage: 'A range of values on the visualization',
   }),
 };
diff --git a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts
index e1e7b6507d82..e63ff28f42d9 100644
--- a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts
+++ b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts
@@ -27,6 +27,6 @@ export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = {
     defaultMessage: 'Single click',
   }),
   description: i18n.translate('uiActions.triggers.valueClickDescription', {
-    defaultMessage: 'A single point clicked on a visualization',
+    defaultMessage: 'A single point on the visualization',
   }),
 };
diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js
index 94a271987ecd..faf272daba09 100644
--- a/test/functional/apps/discover/_discover.js
+++ b/test/functional/apps/discover/_discover.js
@@ -224,9 +224,7 @@ export default function ({ getService, getPageObjects }) {
         await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' });
         await PageObjects.common.navigateToApp('discover');
         await PageObjects.header.awaitKibanaChrome();
-        await queryBar.setQuery('');
-        // To remove focus of the of the search bar so date/time picker can show
-        await PageObjects.discover.selectIndexPattern(defaultSettings.defaultIndex);
+        await queryBar.clearQuery();
         await PageObjects.timePicker.setDefaultAbsoluteRange();
 
         log.debug(
diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts
index 06828e8e98cc..bfe0da7a5b24 100644
--- a/test/functional/apps/visualize/_tsvb_chart.ts
+++ b/test/functional/apps/visualize/_tsvb_chart.ts
@@ -48,8 +48,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
       await PageObjects.visualBuilder.checkVisualBuilderIsPresent();
     });
 
-    // FLAKY: https://github.com/elastic/kibana/issues/75127
-    describe.skip('metric', () => {
+    describe('metric', () => {
       beforeEach(async () => {
         await PageObjects.visualBuilder.resetPage();
         await PageObjects.visualBuilder.clickMetric();
diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts
index ba60aa83d92d..4f12a45cf5f6 100644
--- a/test/functional/apps/visualize/_tsvb_markdown.ts
+++ b/test/functional/apps/visualize/_tsvb_markdown.ts
@@ -30,10 +30,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
     await visualBuilder.markdownSwitchSubTab('markdown');
     const rerenderedTable = await visualBuilder.getMarkdownTableVariables();
     rerenderedTable.forEach((row) => {
-      // eslint-disable-next-line no-unused-expressions
-      variableName === 'label'
-        ? expect(row.key).to.include.string(checkedValue)
-        : expect(row.key).to.not.include.string(checkedValue);
+      if (variableName === 'label') {
+        expect(row.key).to.include.string(checkedValue);
+      } else {
+        expect(row.key).to.not.include.string(checkedValue);
+      }
     });
   }
 
@@ -107,10 +108,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
 
         table.forEach((row, index) => {
           // exception: last index for variable is always: {{count.label}}
-          // eslint-disable-next-line no-unused-expressions
-          index === table.length - 1
-            ? expect(row.key).to.not.include.string(VARIABLE)
-            : expect(row.key).to.include.string(VARIABLE);
+          if (index === table.length - 1) {
+            expect(row.key).to.not.include.string(VARIABLE);
+          } else {
+            expect(row.key).to.include.string(VARIABLE);
+          }
         });
 
         await cleanupMarkdownData(VARIABLE, VARIABLE);
diff --git a/test/functional/apps/visualize/input_control_vis/chained_controls.js b/test/functional/apps/visualize/input_control_vis/chained_controls.js
index 035245b50d43..e1a58e1da34f 100644
--- a/test/functional/apps/visualize/input_control_vis/chained_controls.js
+++ b/test/functional/apps/visualize/input_control_vis/chained_controls.js
@@ -26,8 +26,7 @@ export default function ({ getService, getPageObjects }) {
   const find = getService('find');
   const comboBox = getService('comboBox');
 
-  // FLAKY: https://github.com/elastic/kibana/issues/68472
-  describe.skip('chained controls', function () {
+  describe('chained controls', function () {
     this.tags('includeFirefox');
 
     before(async () => {
diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts
index ce7a3f9e132f..31f4e393f019 100644
--- a/test/functional/page_objects/common_page.ts
+++ b/test/functional/page_objects/common_page.ts
@@ -346,6 +346,10 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
       await browser.pressKeys(browser.keys.ENTER);
     }
 
+    async pressTabKey() {
+      await browser.pressKeys(browser.keys.TAB);
+    }
+
     // Pause the browser at a certain place for debugging
     // Not meant for usage in CI, only for dev-usage
     async pause() {
diff --git a/test/functional/services/query_bar.ts b/test/functional/services/query_bar.ts
index 7c7fd2d81f17..8cd63fb2f4a5 100644
--- a/test/functional/services/query_bar.ts
+++ b/test/functional/services/query_bar.ts
@@ -54,6 +54,11 @@ export function QueryBarProvider({ getService, getPageObjects }: FtrProviderCont
       });
     }
 
+    public async clearQuery(): Promise<void> {
+      await this.setQuery('');
+      await PageObjects.common.pressTabKey();
+    }
+
     public async submitQuery(): Promise<void> {
       log.debug('QueryBar.submitQuery');
       await testSubjects.click('queryInput');
diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx
deleted file mode 100644
index 58916f26121d..000000000000
--- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import { EuiCallOut, EuiFieldText, EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui';
-import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public';
-import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public';
-import { ChartActionContext } from '../../../../../src/plugins/embeddable/public';
-import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public';
-import {
-  SELECT_RANGE_TRIGGER,
-  VALUE_CLICK_TRIGGER,
-} from '../../../../../src/plugins/ui_actions/public';
-import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public';
-
-function isValidUrl(url: string) {
-  try {
-    new URL(url);
-    return true;
-  } catch {
-    return false;
-  }
-}
-
-export type ActionContext = ChartActionContext;
-
-export interface Config {
-  url: string;
-  openInNewTab: boolean;
-}
-
-type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER;
-
-export type CollectConfigProps = CollectConfigPropsBase<Config, { triggers: UrlTrigger[] }>;
-
-const SAMPLE_DASHBOARD_TO_URL_DRILLDOWN = 'SAMPLE_DASHBOARD_TO_URL_DRILLDOWN';
-
-export class DashboardToUrlDrilldown implements Drilldown<Config, UrlTrigger> {
-  public readonly id = SAMPLE_DASHBOARD_TO_URL_DRILLDOWN;
-
-  public readonly order = 8;
-
-  readonly minimalLicense = 'gold'; // example of minimal license support
-  readonly licenseFeatureName = 'Sample URL Drilldown';
-
-  public readonly getDisplayName = () => 'Go to URL (example)';
-
-  public readonly euiIcon = 'link';
-
-  supportedTriggers(): UrlTrigger[] {
-    return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER];
-  }
-
-  private readonly ReactCollectConfig: React.FC<CollectConfigProps> = ({
-    config,
-    onConfig,
-    context,
-  }) => (
-    <>
-      <EuiCallOut title="Example warning!" color="warning" iconType="help">
-        <p>
-          This is an example drilldown. It is meant as a starting point for developers, so they can
-          grab this code and get started. It does not provide a complete working functionality but
-          serves as a getting started example.
-        </p>
-        <p>
-          Implementation of the actual <em>Go to URL</em> drilldown is tracked in{' '}
-          <a href="https://github.com/elastic/kibana/issues/55324">#55324</a>
-        </p>
-      </EuiCallOut>
-      <EuiSpacer size="xl" />
-      <EuiFormRow label="Enter target URL" fullWidth>
-        <EuiFieldText
-          fullWidth
-          name="url"
-          placeholder="Enter URL"
-          value={config.url}
-          onChange={(event) => onConfig({ ...config, url: event.target.value })}
-          onBlur={() => {
-            if (!config.url) return;
-            if (/https?:\/\//.test(config.url)) return;
-            onConfig({ ...config, url: 'https://' + config.url });
-          }}
-        />
-      </EuiFormRow>
-      <EuiFormRow hasChildLabel={false}>
-        <EuiSwitch
-          name="openInNewTab"
-          label="Open in new tab?"
-          checked={config.openInNewTab}
-          onChange={() => onConfig({ ...config, openInNewTab: !config.openInNewTab })}
-        />
-      </EuiFormRow>
-      <EuiSpacer size="xl" />
-      <EuiCallOut>
-        {/* just demo how can access selected triggers*/}
-        <p>Will be attached to triggers: {JSON.stringify(context.triggers)}</p>
-      </EuiCallOut>
-    </>
-  );
-
-  public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig);
-
-  public readonly createConfig = () => ({
-    url: '',
-    openInNewTab: false,
-  });
-
-  public readonly isConfigValid = (config: Config): config is Config => {
-    if (!config.url) return false;
-    return isValidUrl(config.url);
-  };
-
-  /**
-   * `getHref` is need to support mouse middle-click and Cmd + Click behavior
-   * to open a link in new tab.
-   */
-  public readonly getHref = async (config: Config, context: ActionContext) => {
-    return config.url;
-  };
-
-  public readonly execute = async (
-    config: Config,
-    context: ActionExecutionContext<ActionContext>
-  ) => {
-    // Just for showcasing:
-    // we can get trigger a which caused this drilldown execution
-    // eslint-disable-next-line no-console
-    console.log(context.trigger?.id);
-
-    const url = await this.getHref(config, context);
-
-    if (config.openInNewTab) {
-      window.open(url, '_blank');
-    } else {
-      window.location.href = url;
-    }
-  };
-}
diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts
index 7f2c9a9b3bbc..3f0b64a2ac9e 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts
+++ b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts
@@ -11,7 +11,6 @@ import {
   AdvancedUiActionsStart,
 } from '../../../../x-pack/plugins/ui_actions_enhanced/public';
 import { DashboardHelloWorldDrilldown } from './dashboard_hello_world_drilldown';
-import { DashboardToUrlDrilldown } from './dashboard_to_url_drilldown';
 import { DashboardToDiscoverDrilldown } from './dashboard_to_discover_drilldown';
 import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public';
 import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public';
@@ -39,7 +38,6 @@ export class UiActionsEnhancedExamplesPlugin
 
     uiActions.registerDrilldown(new DashboardHelloWorldDrilldown());
     uiActions.registerDrilldown(new DashboardHelloWorldOnlyRangeSelectDrilldown());
-    uiActions.registerDrilldown(new DashboardToUrlDrilldown());
     uiActions.registerDrilldown(new DashboardToDiscoverDrilldown({ start }));
   }
 
diff --git a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap
index 708758f2c6e5..e9763082a399 100644
--- a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap
+++ b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap
@@ -536,61 +536,54 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the
                             },
                             "transaction_count": {
                               "type": "long"
-                            }
-                          }
-                        },
-                        "no_observer_name": {
-                          "properties": {
-                            "expected_metric_document_count": {
-                              "type": "long"
                             },
-                            "transaction_count": {
+                            "ratio": {
                               "type": "long"
                             }
                           }
                         },
-                        "no_rum": {
+                        "no_observer_name": {
                           "properties": {
                             "expected_metric_document_count": {
                               "type": "long"
                             },
                             "transaction_count": {
                               "type": "long"
-                            }
-                          }
-                        },
-                        "no_rum_no_observer_name": {
-                          "properties": {
-                            "expected_metric_document_count": {
-                              "type": "long"
                             },
-                            "transaction_count": {
+                            "ratio": {
                               "type": "long"
                             }
                           }
                         },
-                        "only_rum": {
+                        "with_country": {
                           "properties": {
                             "expected_metric_document_count": {
                               "type": "long"
                             },
                             "transaction_count": {
                               "type": "long"
-                            }
-                          }
-                        },
-                        "only_rum_no_observer_name": {
-                          "properties": {
-                            "expected_metric_document_count": {
-                              "type": "long"
                             },
-                            "transaction_count": {
+                            "ratio": {
                               "type": "long"
                             }
                           }
                         }
                       }
                     },
+                    "environments": {
+                      "properties": {
+                        "services_without_environment": {
+                          "type": "long"
+                        },
+                        "services_with_multiple_environments": {
+                          "type": "long"
+                        },
+                        "top_enviroments": {
+                          "type": "keyword",
+                          "ignore_above": 1024
+                        }
+                      }
+                    },
                     "cloud": {
                       "properties": {
                         "availability_zone": {
@@ -952,6 +945,17 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the
                             }
                           }
                         },
+                        "environments": {
+                          "properties": {
+                            "took": {
+                              "properties": {
+                                "ms": {
+                                  "type": "long"
+                                }
+                              }
+                            }
+                          }
+                        },
                         "groupings": {
                           "properties": {
                             "took": {
diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
index aecf4af66760..48ff69d3afcb 100644
--- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
+++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
@@ -68,9 +68,9 @@ exports[`Error METRIC_SYSTEM_FREE_MEMORY 1`] = `undefined`;
 
 exports[`Error METRIC_SYSTEM_TOTAL_MEMORY 1`] = `undefined`;
 
-exports[`Error OBSERVER_LISTENING 1`] = `undefined`;
+exports[`Error OBSERVER_HOSTNAME 1`] = `undefined`;
 
-exports[`Error OBSERVER_NAME 1`] = `"an observer"`;
+exports[`Error OBSERVER_LISTENING 1`] = `undefined`;
 
 exports[`Error OBSERVER_VERSION_MAJOR 1`] = `8`;
 
@@ -220,9 +220,9 @@ exports[`Span METRIC_SYSTEM_FREE_MEMORY 1`] = `undefined`;
 
 exports[`Span METRIC_SYSTEM_TOTAL_MEMORY 1`] = `undefined`;
 
-exports[`Span OBSERVER_LISTENING 1`] = `undefined`;
+exports[`Span OBSERVER_HOSTNAME 1`] = `undefined`;
 
-exports[`Span OBSERVER_NAME 1`] = `"an observer"`;
+exports[`Span OBSERVER_LISTENING 1`] = `undefined`;
 
 exports[`Span OBSERVER_VERSION_MAJOR 1`] = `8`;
 
@@ -372,9 +372,9 @@ exports[`Transaction METRIC_SYSTEM_FREE_MEMORY 1`] = `undefined`;
 
 exports[`Transaction METRIC_SYSTEM_TOTAL_MEMORY 1`] = `undefined`;
 
-exports[`Transaction OBSERVER_LISTENING 1`] = `undefined`;
+exports[`Transaction OBSERVER_HOSTNAME 1`] = `undefined`;
 
-exports[`Transaction OBSERVER_NAME 1`] = `"an observer"`;
+exports[`Transaction OBSERVER_LISTENING 1`] = `undefined`;
 
 exports[`Transaction OBSERVER_VERSION_MAJOR 1`] = `8`;
 
diff --git a/x-pack/plugins/apm/common/apm_telemetry.ts b/x-pack/plugins/apm/common/apm_telemetry.ts
index 318b956cd3b3..3e885f4948c1 100644
--- a/x-pack/plugins/apm/common/apm_telemetry.ts
+++ b/x-pack/plugins/apm/common/apm_telemetry.ts
@@ -78,6 +78,7 @@ export function getApmTelemetryMapping() {
     properties: {
       expected_metric_document_count: long,
       transaction_count: long,
+      ratio: long,
     },
   };
 
@@ -102,10 +103,14 @@ export function getApmTelemetryMapping() {
         properties: {
           current_implementation: aggregatedTransactionsProperties,
           no_observer_name: aggregatedTransactionsProperties,
-          no_rum: aggregatedTransactionsProperties,
-          no_rum_no_observer_name: aggregatedTransactionsProperties,
-          only_rum: aggregatedTransactionsProperties,
-          only_rum_no_observer_name: aggregatedTransactionsProperties,
+          with_country: aggregatedTransactionsProperties,
+        },
+      },
+      environments: {
+        properties: {
+          services_without_environment: long,
+          services_with_multiple_environments: long,
+          top_enviroments: keyword,
         },
       },
       cloud: {
@@ -227,6 +232,7 @@ export function getApmTelemetryMapping() {
           agents: tookProperties,
           cardinality: tookProperties,
           cloud: tookProperties,
+          environments: tookProperties,
           groupings: tookProperties,
           indices_stats: tookProperties,
           integrations: tookProperties,
diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
index 4aa68de9b8b3..f7b838df9ea2 100644
--- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
+++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
@@ -31,7 +31,7 @@ export const USER_AGENT_NAME = 'user_agent.name';
 
 export const DESTINATION_ADDRESS = 'destination.address';
 
-export const OBSERVER_NAME = 'observer.name';
+export const OBSERVER_HOSTNAME = 'observer.hostname';
 export const OBSERVER_VERSION_MAJOR = 'observer.version_major';
 export const OBSERVER_LISTENING = 'observer.listening';
 export const PROCESSOR_EVENT = 'processor.event';
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx
index 7e5e7cdc53c5..12d8efdbd27f 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx
@@ -5,64 +5,85 @@
  */
 
 import React from 'react';
-import { BreakdownGroup } from './BreakdownGroup';
-import { BreakdownItem } from '../../../../../typings/ui_filters';
+import { EuiSuperSelect } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
 import {
   CLIENT_GEO_COUNTRY_ISO_CODE,
   USER_AGENT_DEVICE,
   USER_AGENT_NAME,
   USER_AGENT_OS,
 } from '../../../../../common/elasticsearch_fieldnames';
+import { BreakdownItem } from '../../../../../typings/ui_filters';
 
 interface Props {
-  id: string;
-  selectedBreakdowns: BreakdownItem[];
-  onBreakdownChange: (values: BreakdownItem[]) => void;
+  selectedBreakdown: BreakdownItem | null;
+  onBreakdownChange: (value: BreakdownItem | null) => void;
 }
 
 export function BreakdownFilter({
-  id,
-  selectedBreakdowns,
+  selectedBreakdown,
   onBreakdownChange,
 }: Props) {
-  const categories: BreakdownItem[] = [
+  const NO_BREAKDOWN = 'noBreakdown';
+
+  const items: BreakdownItem[] = [
     {
-      name: 'Browser',
+      name: i18n.translate('xpack.apm.csm.breakDownFilter.noBreakdown', {
+        defaultMessage: 'No breakdown',
+      }),
+      fieldName: NO_BREAKDOWN,
       type: 'category',
-      count: 0,
-      selected: selectedBreakdowns.some(({ name }) => name === 'Browser'),
-      fieldName: USER_AGENT_NAME,
     },
     {
-      name: 'OS',
+      name: i18n.translate('xpack.apm.csm.breakdownFilter.browser', {
+        defaultMessage: 'Browser',
+      }),
+      fieldName: USER_AGENT_NAME,
       type: 'category',
-      count: 0,
-      selected: selectedBreakdowns.some(({ name }) => name === 'OS'),
-      fieldName: USER_AGENT_OS,
     },
     {
-      name: 'Device',
+      name: i18n.translate('xpack.apm.csm.breakdownFilter.os', {
+        defaultMessage: 'OS',
+      }),
+      fieldName: USER_AGENT_OS,
       type: 'category',
-      count: 0,
-      selected: selectedBreakdowns.some(({ name }) => name === 'Device'),
-      fieldName: USER_AGENT_DEVICE,
     },
     {
-      name: 'Location',
+      name: i18n.translate('xpack.apm.csm.breakdownFilter.device', {
+        defaultMessage: 'Device',
+      }),
+      fieldName: USER_AGENT_DEVICE,
       type: 'category',
-      count: 0,
-      selected: selectedBreakdowns.some(({ name }) => name === 'Location'),
+    },
+    {
+      name: i18n.translate('xpack.apm.csm.breakdownFilter.location', {
+        defaultMessage: 'Location',
+      }),
       fieldName: CLIENT_GEO_COUNTRY_ISO_CODE,
+      type: 'category',
     },
   ];
 
+  const options = items.map(({ name, fieldName }) => ({
+    inputDisplay: fieldName === NO_BREAKDOWN ? name : <strong>{name}</strong>,
+    value: fieldName,
+    dropdownDisplay: name,
+  }));
+
+  const onOptionChange = (value: string) => {
+    if (value === NO_BREAKDOWN) {
+      onBreakdownChange(null);
+    }
+    onBreakdownChange(items.find(({ fieldName }) => fieldName === value)!);
+  };
+
   return (
-    <BreakdownGroup
-      id={id}
-      items={categories}
-      onChange={(selValues: BreakdownItem[]) => {
-        onBreakdownChange(selValues);
-      }}
+    <EuiSuperSelect
+      fullWidth
+      compressed
+      options={options}
+      valueOfSelected={selectedBreakdown?.fieldName ?? NO_BREAKDOWN}
+      onChange={(value) => onOptionChange(value)}
     />
   );
 }
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx
deleted file mode 100644
index d4f80667ce98..000000000000
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import {
-  EuiPopover,
-  EuiFilterButton,
-  EuiFilterGroup,
-  EuiPopoverTitle,
-  EuiFilterSelectItem,
-} from '@elastic/eui';
-import React, { MouseEvent, useCallback, useEffect, useState } from 'react';
-import { BreakdownItem } from '../../../../../typings/ui_filters';
-import { I18LABELS } from '../translations';
-
-export interface BreakdownGroupProps {
-  id: string;
-  disabled?: boolean;
-  items: BreakdownItem[];
-  onChange: (values: BreakdownItem[]) => void;
-}
-
-export function BreakdownGroup({
-  id,
-  disabled,
-  onChange,
-  items,
-}: BreakdownGroupProps) {
-  const [isOpen, setIsOpen] = useState<boolean>(false);
-
-  const [activeItems, setActiveItems] = useState<BreakdownItem[]>(items);
-
-  useEffect(() => {
-    setActiveItems(items);
-  }, [items]);
-
-  const getSelItems = () => activeItems.filter((item) => item.selected);
-
-  const onFilterItemClick = useCallback(
-    (name: string) => (_event: MouseEvent<HTMLButtonElement>) => {
-      setActiveItems((prevItems) =>
-        prevItems.map((item) => ({
-          ...item,
-          selected: name === item.name ? !item.selected : item.selected,
-        }))
-      );
-    },
-    []
-  );
-
-  return (
-    <EuiFilterGroup>
-      <EuiPopover
-        button={
-          <EuiFilterButton
-            isDisabled={disabled && getSelItems().length === 0}
-            isSelected={getSelItems().length > 0}
-            numFilters={activeItems.length}
-            numActiveFilters={getSelItems().length}
-            hasActiveFilters={getSelItems().length !== 0}
-            iconType="arrowDown"
-            onClick={() => {
-              setIsOpen(!isOpen);
-            }}
-            size="s"
-          >
-            {I18LABELS.breakdown}
-          </EuiFilterButton>
-        }
-        closePopover={() => {
-          setIsOpen(false);
-          onChange(getSelItems());
-        }}
-        data-cy={`breakdown-popover_${id}`}
-        id={id}
-        isOpen={isOpen}
-        ownFocus={true}
-        withTitle
-        zIndex={10000}
-      >
-        <EuiPopoverTitle>{I18LABELS.selectBreakdown}</EuiPopoverTitle>
-        <div className="euiFilterSelect__items" style={{ minWidth: 200 }}>
-          {activeItems.map(({ name, count, selected }) => (
-            <EuiFilterSelectItem
-              checked={!!selected ? 'on' : undefined}
-              data-cy={`filter-breakdown-item_${name}`}
-              key={name + count}
-              onClick={onFilterItemClick(name)}
-              disabled={!selected && getSelItems().length > 0}
-            >
-              {name}
-            </EuiFilterSelectItem>
-          ))}
-        </div>
-      </EuiPopover>
-    </EuiFilterGroup>
-  );
-}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx
index 33573052dbcb..c832ec9fcc0d 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx
@@ -43,7 +43,7 @@ interface PageLoadData {
 interface Props {
   onPercentileChange: (min: number, max: number) => void;
   data?: PageLoadData | null;
-  breakdowns: BreakdownItem[];
+  breakdown: BreakdownItem | null;
   percentileRange: PercentileRange;
   loading: boolean;
 }
@@ -57,7 +57,7 @@ const PageLoadChart = styled(Chart)`
 export function PageLoadDistChart({
   onPercentileChange,
   data,
-  breakdowns,
+  breakdown,
   loading,
   percentileRange,
 }: Props) {
@@ -122,17 +122,17 @@ export function PageLoadDistChart({
             data={data?.pageLoadDistribution ?? []}
             curve={CurveType.CURVE_CATMULL_ROM}
           />
-          {breakdowns.map(({ name, type }) => (
+          {breakdown && (
             <BreakdownSeries
-              key={`${type}-${name}`}
-              field={type}
-              value={name}
+              key={`${breakdown.type}-${breakdown.name}`}
+              field={breakdown.type}
+              value={breakdown.name}
               percentileRange={percentileRange}
               onLoadingChange={(bLoading) => {
                 setBreakdownLoading(bLoading);
               }}
             />
-          ))}
+          )}
         </PageLoadChart>
       )}
     </ChartWrapper>
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
index 53f2d5ae238c..3e35f1525493 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
@@ -34,7 +34,7 @@ export function PageLoadDistribution() {
     max: null,
   });
 
-  const [breakdowns, setBreakdowns] = useState<BreakdownItem[]>([]);
+  const [breakdown, setBreakdown] = useState<BreakdownItem | null>(null);
 
   const { data, status } = useFetcher(
     (callApmApi) => {
@@ -94,11 +94,10 @@ export function PageLoadDistribution() {
             {I18LABELS.resetZoom}
           </EuiButtonEmpty>
         </EuiFlexItem>
-        <EuiFlexItem grow={false}>
+        <EuiFlexItem grow={false} style={{ width: 170 }}>
           <BreakdownFilter
-            id={'pageLoad'}
-            selectedBreakdowns={breakdowns}
-            onBreakdownChange={setBreakdowns}
+            selectedBreakdown={breakdown}
+            onBreakdownChange={setBreakdown}
           />
         </EuiFlexItem>
       </EuiFlexGroup>
@@ -107,7 +106,7 @@ export function PageLoadDistribution() {
         data={data}
         onPercentileChange={onPercentileChange}
         loading={status !== 'success'}
-        breakdowns={breakdowns}
+        breakdown={breakdown}
         percentileRange={{
           max: percentileRange.max || data?.maxDuration,
           min: percentileRange.min || data?.minDuration,
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
index 0f43c0ddf540..a67f6dd8e3cb 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
@@ -18,7 +18,7 @@ export function PageViewsTrend() {
 
   const { start, end, serviceName } = urlParams;
 
-  const [breakdowns, setBreakdowns] = useState<BreakdownItem[]>([]);
+  const [breakdown, setBreakdown] = useState<BreakdownItem | null>(null);
 
   const { data, status } = useFetcher(
     (callApmApi) => {
@@ -30,9 +30,9 @@ export function PageViewsTrend() {
               start,
               end,
               uiFilters: JSON.stringify(uiFilters),
-              ...(breakdowns.length > 0
+              ...(breakdown
                 ? {
-                    breakdowns: JSON.stringify(breakdowns),
+                    breakdowns: JSON.stringify(breakdown),
                   }
                 : {}),
             },
@@ -41,13 +41,9 @@ export function PageViewsTrend() {
       }
       return Promise.resolve(undefined);
     },
-    [end, start, serviceName, uiFilters, breakdowns]
+    [end, start, serviceName, uiFilters, breakdown]
   );
 
-  const onBreakdownChange = (values: BreakdownItem[]) => {
-    setBreakdowns(values);
-  };
-
   return (
     <div>
       <EuiFlexGroup responsive={false}>
@@ -56,11 +52,10 @@ export function PageViewsTrend() {
             <h3>{I18LABELS.pageViews}</h3>
           </EuiTitle>
         </EuiFlexItem>
-        <EuiFlexItem grow={false}>
+        <EuiFlexItem grow={false} style={{ width: 170 }}>
           <BreakdownFilter
-            id={'pageView'}
-            selectedBreakdowns={breakdowns}
-            onBreakdownChange={onBreakdownChange}
+            selectedBreakdown={breakdown}
+            onBreakdownChange={setBreakdown}
           />
         </EuiFlexItem>
       </EuiFlexGroup>
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx
new file mode 100644
index 000000000000..bc9df71c534e
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx
@@ -0,0 +1,119 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { storiesOf } from '@storybook/react';
+import React from 'react';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { TransactionGroup } from '../../../../../server/lib/transaction_groups/fetcher';
+import { TransactionList } from './';
+
+storiesOf('app/TransactionOverview/TransactionList', module).add(
+  'Single Row',
+  () => {
+    const items: TransactionGroup[] = [
+      {
+        name:
+          'GET /api/v1/regions/azure-eastus2/clusters/elasticsearch/xc18de071deb4262be54baebf5f6a1ce/proxy/_snapshot/found-snapshots/_all',
+        sample: {
+          container: {
+            id:
+              'xa802046074071c9c828e8db3b7ef92ea0484d9fe783b9c518f65a7b45dfdd2c',
+          },
+          agent: {
+            name: 'java',
+            ephemeral_id: 'x787d6b7-3241-4b55-ba49-0c96bc9857d1',
+            version: '1.17.0',
+          },
+          process: {
+            pid: 28,
+            title: '/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java',
+          },
+          processor: {
+            name: 'transaction',
+            event: 'transaction',
+          },
+          labels: {
+            path:
+              '/api/v1/regions/azure-eastus2/clusters/elasticsearch/xc18de071deb4262be54baebf5f6a1ce/proxy/_snapshot/found-snapshots/_all',
+            status_code: '200',
+            request_method: 'GET',
+            request_id: 'x273dc2477e021979125e0ec67e8d778',
+          },
+          observer: {
+            hostname: 'x840922c967b',
+            name: 'instance-000000000x',
+            id: 'xb384baf-c16a-415a-928a-a10635a04b81',
+            ephemeral_id: 'x9227f0e-848d-423e-a65a-5fdee321f4a9',
+            type: 'apm-server',
+            version: '7.8.1',
+            version_major: 7,
+          },
+          trace: {
+            id: 'x998d7e5db84aa8341b358a264a78984',
+          },
+          '@timestamp': '2020-08-26T14:40:31.472Z',
+          ecs: {
+            version: '1.5.0',
+          },
+          service: {
+            node: {
+              name:
+                'xa802046074071c9c828e8db3b7ef92ea0484d9fe783b9c518f65a7b45dfdd2c',
+            },
+            environment: 'qa',
+            framework: {
+              name: 'API',
+            },
+            name: 'adminconsole',
+            runtime: {
+              name: 'Java',
+              version: '1.8.0_265',
+            },
+            language: {
+              name: 'Java',
+              version: '1.8.0_265',
+            },
+            version: 'ms-44.1-BC_1',
+          },
+          host: {
+            hostname: 'xa8020460740',
+            os: {
+              platform: 'Linux',
+            },
+            ip: '3.83.239.24',
+            name: 'xa8020460740',
+            architecture: 'amd64',
+          },
+          transaction: {
+            duration: {
+              us: 8260617,
+            },
+            result: 'HTTP 2xx',
+            name:
+              'GET /api/v1/regions/azure-eastus2/clusters/elasticsearch/xc18de071deb4262be54baebf5f6a1ce/proxy/_snapshot/found-snapshots/_all',
+            span_count: {
+              dropped: 0,
+              started: 8,
+            },
+            id: 'xaa3cae6fd4f7023',
+            type: 'request',
+            sampled: true,
+          },
+          timestamp: {
+            us: 1598452831472001,
+          },
+        },
+        p95: 11974156,
+        averageResponseTime: 8087434.558974359,
+        transactionsPerMinute: 0.40625,
+        impact: 100,
+        impactRelative: 100,
+      },
+    ];
+
+    return <TransactionList isLoading={false} items={items} />;
+  }
+);
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx
similarity index 85%
rename from x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx
rename to x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx
index 2b1c1b8e8c11..d8c6d7d28fa9 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx
@@ -19,9 +19,16 @@ import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
 import { EmptyMessage } from '../../../shared/EmptyMessage';
 import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink';
 
+// Truncate both the link and the child span (the tooltip anchor.) The link so
+// it doesn't overflow, and the anchor so we get the ellipsis.
 const TransactionNameLink = styled(TransactionDetailLink)`
-  ${truncate('100%')};
   font-family: ${fontFamilyCode};
+  white-space: nowrap;
+  ${truncate('100%')};
+
+  > span {
+    ${truncate('100%')};
+  }
 `;
 
 interface Props {
@@ -41,20 +48,20 @@ export function TransactionList({ items, isLoading }: Props) {
         sortable: true,
         render: (_, { sample }: TransactionGroup) => {
           return (
-            <EuiToolTip
-              id="transaction-name-link-tooltip"
-              content={sample.transaction.name || NOT_AVAILABLE_LABEL}
+            <TransactionNameLink
+              serviceName={sample.service.name}
+              transactionId={sample.transaction.id}
+              traceId={sample.trace.id}
+              transactionName={sample.transaction.name}
+              transactionType={sample.transaction.type}
             >
-              <TransactionNameLink
-                serviceName={sample.service.name}
-                transactionId={sample.transaction.id}
-                traceId={sample.trace.id}
-                transactionName={sample.transaction.name}
-                transactionType={sample.transaction.type}
+              <EuiToolTip
+                id="transaction-name-link-tooltip"
+                content={sample.transaction.name || NOT_AVAILABLE_LABEL}
               >
-                {sample.transaction.name || NOT_AVAILABLE_LABEL}
-              </TransactionNameLink>
-            </EuiToolTip>
+                <>{sample.transaction.name || NOT_AVAILABLE_LABEL}</>
+              </EuiToolTip>
+            </TransactionNameLink>
           );
         },
       },
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
index 81d8a6f80737..5999988abe84 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
@@ -33,7 +33,7 @@ import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
 import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
 import { LocalUIFilters } from '../../shared/LocalUIFilters';
 import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter';
-import { TransactionList } from './List';
+import { TransactionList } from './TransactionList';
 import { useRedirect } from './useRedirect';
 
 function getRedirectLocation({
@@ -62,6 +62,7 @@ function getRedirectLocation({
 export function TransactionOverview() {
   const location = useLocation();
   const { urlParams } = useUrlParams();
+
   const { serviceName, transactionType } = urlParams;
 
   // TODO: fetching of transaction types should perhaps be lifted since it is needed in several places. Context?
diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/ApmHeader.stories.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/ApmHeader.stories.tsx
new file mode 100644
index 000000000000..c9b7c7740984
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/ApmHeader.stories.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiTitle } from '@elastic/eui';
+import { storiesOf } from '@storybook/react';
+import React from 'react';
+import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext';
+import { ApmHeader } from './';
+
+storiesOf('shared/ApmHeader', module)
+  .addDecorator((storyFn) => {
+    return (
+      <MockApmPluginContextWrapper>{storyFn()}</MockApmPluginContextWrapper>
+    );
+  })
+  .add('Example', () => {
+    return (
+      <ApmHeader>
+        <EuiTitle size="l">
+          <h1>
+            GET
+            /api/v1/regions/azure-eastus2/clusters/elasticsearch/xc18de071deb4262be54baebf5f6a1ce/proxy/_snapshot/found-snapshots/_all
+          </h1>
+        </EuiTitle>
+      </ApmHeader>
+    );
+  });
diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx
index 4ffd42280181..9f67ba99103e 100644
--- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx
@@ -6,15 +6,25 @@
 
 import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
 import React, { ReactNode } from 'react';
-import { KueryBar } from '../KueryBar';
+import styled from 'styled-components';
 import { DatePicker } from '../DatePicker';
 import { EnvironmentFilter } from '../EnvironmentFilter';
+import { KueryBar } from '../KueryBar';
+
+// Header titles with long, unbroken words, like you would see for a long URL in
+// a transaction name, with the default `work-break`, don't break, and that ends
+// up pushing the date picker off of the screen. Setting `break-all` here lets
+// it wrap even if it has a long, unbroken work. The wrapped result is not great
+// looking, since it wraps, but it doesn't push any controls off of the screen.
+const ChildrenContainerFlexItem = styled(EuiFlexItem)`
+  word-break: break-all;
+`;
 
 export function ApmHeader({ children }: { children: ReactNode }) {
   return (
     <>
       <EuiFlexGroup alignItems="center" gutterSize="s">
-        <EuiFlexItem>{children}</EuiFlexItem>
+        <ChildrenContainerFlexItem>{children}</ChildrenContainerFlexItem>
         <EuiFlexItem grow={false}>
           <DatePicker />
         </EuiFlexItem>
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx
index da9adbb8dfea..081654186536 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx
@@ -19,7 +19,7 @@ import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/
 // our current storybook setup has core-js-related problems when trying to import
 // it.
 // storiesOf('app/TransactionDurationAlertTrigger', module).add('example',
-// eslint-disable-next-line no-unused-expressions
+// eslint-disable-next-line @typescript-eslint/no-unused-expressions
 () => {
   const params = {
     threshold: 1500,
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
index 9d06fc2ad930..f0ae8467b215 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
@@ -4,9 +4,12 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { AGENT_NAME } from '../../../../common/elasticsearch_fieldnames';
 import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices';
 import { tasks } from './tasks';
+import {
+  SERVICE_NAME,
+  SERVICE_ENVIRONMENT,
+} from '../../../../common/elasticsearch_fieldnames';
 
 describe('data telemetry collection tasks', () => {
   const indices = {
@@ -18,103 +21,136 @@ describe('data telemetry collection tasks', () => {
     /* eslint-enable @typescript-eslint/naming-convention */
   } as ApmIndicesConfig;
 
+  describe('environments', () => {
+    const task = tasks.find((t) => t.name === 'environments');
+
+    it('returns environment information', async () => {
+      const search = jest.fn().mockResolvedValueOnce({
+        aggregations: {
+          environments: {
+            buckets: [
+              {
+                key: 'production',
+              },
+              {
+                key: 'testing',
+              },
+            ],
+          },
+          service_environments: {
+            buckets: [
+              {
+                key: {
+                  [SERVICE_NAME]: 'opbeans-node',
+                  [SERVICE_ENVIRONMENT]: 'production',
+                },
+              },
+              {
+                key: {
+                  [SERVICE_NAME]: 'opbeans-node',
+                  [SERVICE_ENVIRONMENT]: null,
+                },
+              },
+              {
+                key: {
+                  [SERVICE_NAME]: 'opbeans-java',
+                  [SERVICE_ENVIRONMENT]: 'production',
+                },
+              },
+              {
+                key: {
+                  [SERVICE_NAME]: 'opbeans-rum',
+                  [SERVICE_ENVIRONMENT]: null,
+                },
+              },
+            ],
+          },
+        },
+      });
+
+      expect(await task?.executor({ search, indices } as any)).toEqual({
+        environments: {
+          services_with_multiple_environments: 1,
+          services_without_environment: 2,
+          top_environments: ['production', 'testing'],
+        },
+      });
+    });
+  });
+
   describe('aggregated_transactions', () => {
     const task = tasks.find((t) => t.name === 'aggregated_transactions');
 
-    it('returns aggregated transaction counts', async () => {
-      // This mock implementation returns different values based on the parameters,
-      // which should simulate all the queries that are done. For most of them we'll
-      // simulate the number of buckets by using the length of the key, but for a
-      // couple we'll simulate being paginated by returning an after_key.
-      const search = jest.fn().mockImplementation((params) => {
-        const isRumResult =
-          params.body.query.bool.filter &&
-          params.body.query.bool.filter.some(
-            (filter: any) =>
-              filter.terms && filter.terms[AGENT_NAME]?.includes('rum-js')
-          );
-        const isNonRumResult =
-          params.body.query.bool.filter &&
-          params.body.query.bool.filter.some(
-            (filter: any) =>
-              filter.terms && !filter.terms[AGENT_NAME]?.includes('rum-js')
-          );
-        const isPagedResult =
-          !!params.body.aggs?.current_implementation?.composite.after ||
-          !!params.body.aggs?.no_observer_name?.composite.after;
-        const isTotalResult = 'track_total_hits' in params.body;
-        const key = Object.keys(params.body.aggs ?? [])[0];
-
-        if (isRumResult) {
-          if (isTotalResult) {
-            return Promise.resolve({ hits: { total: { value: 3000 } } });
-          }
-        }
-
-        if (isNonRumResult) {
-          if (isTotalResult) {
-            return Promise.resolve({ hits: { total: { value: 2000 } } });
-          }
-        }
+    describe('without transactions', () => {
+      it('returns an empty result', async () => {
+        const search = jest.fn().mockReturnValueOnce({
+          hits: {
+            hits: [],
+            total: {
+              value: 0,
+            },
+          },
+        });
 
-        if (isPagedResult && key) {
-          return Promise.resolve({
-            hits: { total: { value: key.length } },
-            aggregations: { [key]: { buckets: [{}] } },
-          });
-        }
+        expect(await task?.executor({ indices, search } as any)).toEqual({});
+      });
+    });
 
-        if (isTotalResult) {
-          return Promise.resolve({ hits: { total: { value: 1000 } } });
-        }
+    it('returns aggregated transaction counts', async () => {
+      const search = jest
+        .fn()
+        // The first call to `search` asks for a transaction to get
+        // a fixed date range.
+        .mockReturnValueOnce({
+          hits: {
+            hits: [{ _source: { '@timestamp': new Date().toISOString() } }],
+          },
+          total: {
+            value: 1,
+          },
+        })
+        // Later calls are all composite aggregations. We return 2 pages of
+        // results to test if scrolling works.
+        .mockImplementation((params) => {
+          let arrayLength = 1000;
+          let nextAfter: Record<string, any> = { after_key: {} };
+
+          if (params.body.aggs.transaction_metric_groups.composite.after) {
+            arrayLength = 250;
+            nextAfter = {};
+          }
 
-        if (
-          key === 'current_implementation' ||
-          (key === 'no_observer_name' && !isPagedResult)
-        ) {
           return Promise.resolve({
-            hits: { total: { value: key.length } },
-            aggregations: {
-              [key]: { after_key: {}, buckets: key.split('').map((_) => ({})) },
+            hits: {
+              total: {
+                value: 5000,
+              },
             },
-          });
-        }
-
-        if (key) {
-          return Promise.resolve({
-            hits: { total: { value: key.length } },
             aggregations: {
-              [key]: { buckets: key.split('').map((_) => ({})) },
+              transaction_metric_groups: {
+                buckets: new Array(arrayLength),
+                ...nextAfter,
+              },
             },
           });
-        }
-      });
+        });
 
       expect(await task?.executor({ indices, search } as any)).toEqual({
         aggregated_transactions: {
           current_implementation: {
-            expected_metric_document_count: 23,
-            transaction_count: 1000,
+            expected_metric_document_count: 1250,
+            transaction_count: 5000,
+            ratio: 0.25,
           },
           no_observer_name: {
-            expected_metric_document_count: 17,
-            transaction_count: 1000,
-          },
-          no_rum: {
-            expected_metric_document_count: 6,
-            transaction_count: 2000,
-          },
-          no_rum_no_observer_name: {
-            expected_metric_document_count: 23,
-            transaction_count: 2000,
-          },
-          only_rum: {
-            expected_metric_document_count: 8,
-            transaction_count: 3000,
+            expected_metric_document_count: 1250,
+            transaction_count: 5000,
+            ratio: 0.25,
           },
-          only_rum_no_observer_name: {
-            expected_metric_document_count: 25,
-            transaction_count: 3000,
+          with_country: {
+            expected_metric_document_count: 1250,
+            transaction_count: 5000,
+            ratio: 0.25,
           },
         },
       });
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
index 840f47b04341..a53068d152d0 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
@@ -3,7 +3,10 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-import { flatten, merge, sortBy, sum } from 'lodash';
+import { ValuesType } from 'utility-types';
+import { flatten, merge, sortBy, sum, pickBy } from 'lodash';
+import { AggregationOptionsByType } from '../../../../typings/elasticsearch/aggregations';
+import { ProcessorEvent } from '../../../../common/processor_event';
 import { TelemetryTask } from '.';
 import { AGENT_NAMES, RUM_AGENTS } from '../../../../common/agent_name';
 import {
@@ -16,7 +19,7 @@ import {
   CONTAINER_ID,
   ERROR_GROUP_ID,
   HOST_NAME,
-  OBSERVER_NAME,
+  OBSERVER_HOSTNAME,
   PARENT_ID,
   POD_NAME,
   PROCESSOR_EVENT,
@@ -32,10 +35,8 @@ import {
   TRANSACTION_NAME,
   TRANSACTION_RESULT,
   TRANSACTION_TYPE,
-  USER_AGENT_NAME,
   USER_AGENT_ORIGINAL,
 } from '../../../../common/elasticsearch_fieldnames';
-import { ESFilter } from '../../../../typings/elasticsearch';
 import { APMError } from '../../../../typings/es_schemas/ui/apm_error';
 import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
 import { Span } from '../../../../typings/es_schemas/ui/span';
@@ -57,79 +58,114 @@ export const tasks: TelemetryTask[] = [
     // the transaction count for that time range.
     executor: async ({ indices, search }) => {
       async function getBucketCountFromPaginatedQuery(
-        key: string,
-        filter: ESFilter[],
-        count: number = 0,
+        sources: Array<
+          ValuesType<AggregationOptionsByType['composite']['sources']>[string]
+        >,
+        prevResult?: {
+          transaction_count: number;
+          expected_metric_document_count: number;
+        },
         after?: any
-      ) {
+      ): Promise<{
+        transaction_count: number;
+        expected_metric_document_count: number;
+        ratio: number;
+      }> {
+        // eslint-disable-next-line @typescript-eslint/naming-convention
+        let { expected_metric_document_count } = prevResult ?? {
+          transaction_count: 0,
+          expected_metric_document_count: 0,
+        };
+
         const params = {
           index: [indices['apm_oss.transactionIndices']],
           body: {
             size: 0,
             timeout,
-            query: { bool: { filter } },
+            query: {
+              bool: {
+                filter: [
+                  { term: { [PROCESSOR_EVENT]: 'transaction' } },
+                  { range: { '@timestamp': { gte: start, lt: end } } },
+                ],
+              },
+            },
+            track_total_hits: true,
             aggs: {
-              [key]: {
+              transaction_metric_groups: {
                 composite: {
                   ...(after ? { after } : {}),
                   size: 10000,
-                  sources: fieldMap[key].map((field) => ({
-                    [field]: { terms: { field, missing_bucket: true } },
-                  })),
+                  sources: sources.map((source, index) => {
+                    return {
+                      [index]: source,
+                    };
+                  }),
                 },
               },
             },
           },
         };
+
         const result = await search(params);
+
         let nextAfter: any;
 
         if (result.aggregations) {
-          nextAfter = result.aggregations[key].after_key;
-          count += result.aggregations[key].buckets.length;
+          nextAfter = result.aggregations.transaction_metric_groups.after_key;
+          expected_metric_document_count +=
+            result.aggregations.transaction_metric_groups.buckets.length;
         }
 
         if (nextAfter) {
-          count = await getBucketCountFromPaginatedQuery(
-            key,
-            filter,
-            count,
+          return await getBucketCountFromPaginatedQuery(
+            sources,
+            {
+              expected_metric_document_count,
+              transaction_count: result.hits.total.value,
+            },
             nextAfter
           );
         }
 
-        return count;
+        return {
+          expected_metric_document_count,
+          transaction_count: result.hits.total.value,
+          ratio: expected_metric_document_count / result.hits.total.value,
+        };
       }
 
-      async function totalSearch(filter: ESFilter[]) {
-        const result = await search({
-          index: [indices['apm_oss.transactionIndices']],
+      // fixed date range for reliable results
+      const lastTransaction = (
+        await search({
+          index: indices['apm_oss.transactionIndices'],
           body: {
-            size: 0,
-            timeout,
-            query: { bool: { filter } },
-            track_total_hits: true,
+            query: {
+              bool: {
+                filter: [
+                  { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
+                ],
+              },
+            },
+            size: 1,
+            sort: {
+              '@timestamp': 'desc',
+            },
           },
-        });
+        })
+      ).hits.hits[0] as { _source: { '@timestamp': string } };
 
-        return result.hits.total.value;
+      if (!lastTransaction) {
+        return {};
       }
 
-      const nonRumAgentNames = AGENT_NAMES.filter(
-        (name) => !RUM_AGENTS.includes(name)
-      );
+      const end =
+        new Date(lastTransaction._source['@timestamp']).getTime() -
+        5 * 60 * 1000;
 
-      const filter: ESFilter[] = [
-        { term: { [PROCESSOR_EVENT]: 'transaction' } },
-        { range: { '@timestamp': { gte: 'now-1m' } } },
-      ];
-      const noRumFilter = [
-        ...filter,
-        { terms: { [AGENT_NAME]: nonRumAgentNames } },
-      ];
-      const rumFilter = [...filter, { terms: { [AGENT_NAME]: RUM_AGENTS } }];
+      const start = end - 60 * 1000;
 
-      const baseFields = [
+      const simpleTermFields = [
         TRANSACTION_NAME,
         TRANSACTION_RESULT,
         TRANSACTION_TYPE,
@@ -139,73 +175,61 @@ export const tasks: TelemetryTask[] = [
         HOST_NAME,
         CONTAINER_ID,
         POD_NAME,
-      ];
-
-      const fieldMap: Record<string, string[]> = {
-        current_implementation: [OBSERVER_NAME, ...baseFields, USER_AGENT_NAME],
-        no_observer_name: [...baseFields, USER_AGENT_NAME],
-        no_rum: [OBSERVER_NAME, ...baseFields],
-        no_rum_no_observer_name: baseFields,
-        only_rum: [OBSERVER_NAME, ...baseFields, USER_AGENT_NAME],
-        only_rum_no_observer_name: [...baseFields, USER_AGENT_NAME],
-      };
+      ].map((field) => ({ terms: { field, missing_bucket: true } }));
 
-      // It would be more performant to do these in parallel, but we have different filters and keys and it's easier to
-      // understand if we make the code slower and longer
-      const countMap: Record<string, number> = {
-        current_implementation: await getBucketCountFromPaginatedQuery(
-          'current_implementation',
-          filter
-        ),
-        no_observer_name: await getBucketCountFromPaginatedQuery(
-          'no_observer_name',
-          filter
-        ),
-        no_rum: await getBucketCountFromPaginatedQuery('no_rum', noRumFilter),
-        no_rum_no_observer_name: await getBucketCountFromPaginatedQuery(
-          'no_rum_no_observer_name',
-          noRumFilter
-        ),
-        only_rum: await getBucketCountFromPaginatedQuery('only_rum', rumFilter),
-        only_rum_no_observer_name: await getBucketCountFromPaginatedQuery(
-          'only_rum_no_observer_name',
-          rumFilter
-        ),
+      const observerHostname = {
+        terms: { field: OBSERVER_HOSTNAME, missing_bucket: true },
       };
 
-      const [allCount, noRumCount, rumCount] = await Promise.all([
-        totalSearch(filter),
-        totalSearch(noRumFilter),
-        totalSearch(rumFilter),
-      ]);
+      const baseFields = [
+        ...simpleTermFields,
+        // user_agent.name only for page-load transactions
+        {
+          terms: {
+            script: `
+              if (doc['transaction.type'].value == 'page-load' && doc['user_agent.name'].size() > 0) {
+                return doc['user_agent.name'].value;
+              }
 
-      return {
-        aggregated_transactions: {
-          current_implementation: {
-            transaction_count: allCount,
-            expected_metric_document_count: countMap.current_implementation,
-          },
-          no_observer_name: {
-            transaction_count: allCount,
-            expected_metric_document_count: countMap.no_observer_name,
-          },
-          no_rum: {
-            transaction_count: noRumCount,
-            expected_metric_document_count: countMap.no_rum,
+              return null;
+            `,
+            missing_bucket: true,
           },
-          no_rum_no_observer_name: {
-            transaction_count: noRumCount,
-            expected_metric_document_count: countMap.no_rum_no_observer_name,
-          },
-          only_rum: {
-            transaction_count: rumCount,
-            expected_metric_document_count: countMap.only_rum,
-          },
-          only_rum_no_observer_name: {
-            transaction_count: rumCount,
-            expected_metric_document_count: countMap.only_rum_no_observer_name,
+        },
+        // transaction.root
+        {
+          terms: {
+            script: `return doc['parent.id'].size() == 0`,
+            missing_bucket: true,
           },
         },
+      ];
+
+      const results = {
+        current_implementation: await getBucketCountFromPaginatedQuery([
+          ...baseFields,
+          observerHostname,
+        ]),
+        with_country: await getBucketCountFromPaginatedQuery([
+          ...baseFields,
+          observerHostname,
+          {
+            terms: {
+              script: `
+                if (doc['transaction.type'].value == 'page-load' && doc['client.geo.country_iso_code'].size() > 0) {
+                  return doc['client.geo.country_iso_code'].value;
+                }
+                return null;
+              `,
+              missing_bucket: true,
+            },
+          },
+        ]),
+        no_observer_name: await getBucketCountFromPaginatedQuery(baseFields),
+      };
+
+      return {
+        aggregated_transactions: results,
       };
     },
   },
@@ -270,6 +294,87 @@ export const tasks: TelemetryTask[] = [
       return { cloud };
     },
   },
+  {
+    name: 'environments',
+    executor: async ({ indices, search }) => {
+      const response = await search({
+        index: [indices['apm_oss.transactionIndices']],
+        body: {
+          query: {
+            bool: {
+              filter: [{ range: { '@timestamp': { gte: 'now-1d' } } }],
+            },
+          },
+          aggs: {
+            environments: {
+              terms: {
+                field: SERVICE_ENVIRONMENT,
+                size: 5,
+              },
+            },
+            service_environments: {
+              composite: {
+                size: 1000,
+                sources: [
+                  {
+                    [SERVICE_ENVIRONMENT]: {
+                      terms: {
+                        field: SERVICE_ENVIRONMENT,
+                        missing_bucket: true,
+                      },
+                    },
+                  },
+                  {
+                    [SERVICE_NAME]: {
+                      terms: {
+                        field: SERVICE_NAME,
+                      },
+                    },
+                  },
+                ],
+              },
+            },
+          },
+        },
+      });
+
+      const topEnvironments =
+        response.aggregations?.environments.buckets.map(
+          (bucket) => bucket.key
+        ) ?? [];
+      const serviceEnvironments: Record<string, Array<string | null>> = {};
+
+      const buckets = response.aggregations?.service_environments.buckets ?? [];
+
+      buckets.forEach((bucket) => {
+        const serviceName = bucket.key['service.name'];
+        const environment = bucket.key['service.environment'] as string | null;
+
+        const environments = serviceEnvironments[serviceName] ?? [];
+
+        serviceEnvironments[serviceName] = environments.concat(environment);
+      });
+
+      const servicesWithoutEnvironment = Object.keys(
+        pickBy(serviceEnvironments, (environments) =>
+          environments.includes(null)
+        )
+      );
+
+      const servicesWithMultipleEnvironments = Object.keys(
+        pickBy(serviceEnvironments, (environments) => environments.length > 1)
+      );
+
+      return {
+        environments: {
+          services_without_environment: servicesWithoutEnvironment.length,
+          services_with_multiple_environments:
+            servicesWithMultipleEnvironments.length,
+          top_environments: topEnvironments as string[],
+        },
+      };
+    },
+  },
   {
     name: 'processor_events',
     executor: async ({ indices, search }) => {
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts
index 6f4f92c6833f..3463865d326b 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/index.ts
@@ -6,6 +6,7 @@
 import { Observable } from 'rxjs';
 import { take } from 'rxjs/operators';
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { DeepRequired } from 'utility-types';
 import {
   CoreSetup,
   Logger,
@@ -27,6 +28,7 @@ import {
   collectDataTelemetry,
   CollectTelemetryParams,
 } from './collect_data_telemetry';
+import { APMDataTelemetry } from './types';
 
 const APM_TELEMETRY_TASK_NAME = 'apm-telemetry-task';
 
@@ -36,12 +38,14 @@ export async function createApmTelemetry({
   usageCollector,
   taskManager,
   logger,
+  kibanaVersion,
 }: {
   core: CoreSetup;
   config$: Observable<APMConfig>;
   usageCollector: UsageCollectionSetup;
   taskManager: TaskManagerSetupContract;
   logger: Logger;
+  kibanaVersion: string;
 }) {
   taskManager.registerTaskDefinitions({
     [APM_TELEMETRY_TASK_NAME]: {
@@ -95,7 +99,10 @@ export async function createApmTelemetry({
 
     await savedObjectsClient.create(
       APM_TELEMETRY_SAVED_OBJECT_TYPE,
-      dataTelemetry,
+      {
+        ...dataTelemetry,
+        kibanaVersion,
+      },
       { id: APM_TELEMETRY_SAVED_OBJECT_TYPE, overwrite: true }
     );
   };
@@ -105,12 +112,14 @@ export async function createApmTelemetry({
     schema: getApmTelemetryMapping(),
     fetch: async () => {
       try {
-        const data = (
+        const { kibanaVersion: storedKibanaVersion, ...data } = (
           await savedObjectsClient.get(
             APM_TELEMETRY_SAVED_OBJECT_TYPE,
             APM_TELEMETRY_SAVED_OBJECT_ID
           )
-        ).attributes;
+        ).attributes as { kibanaVersion: string } & DeepRequired<
+          APMDataTelemetry
+        >;
 
         return data;
       } catch (err) {
@@ -126,7 +135,7 @@ export async function createApmTelemetry({
 
   usageCollector.registerCollector(collector);
 
-  core.getStartServices().then(([_coreStart, pluginsStart]) => {
+  core.getStartServices().then(async ([_coreStart, pluginsStart]) => {
     const { taskManager: taskManagerStart } = pluginsStart as {
       taskManager: TaskManagerStartContract;
     };
@@ -141,5 +150,25 @@ export async function createApmTelemetry({
       params: {},
       state: {},
     });
+
+    try {
+      const currentData = (
+        await savedObjectsClient.get(
+          APM_TELEMETRY_SAVED_OBJECT_TYPE,
+          APM_TELEMETRY_SAVED_OBJECT_ID
+        )
+      ).attributes as { kibanaVersion?: string };
+
+      if (currentData.kibanaVersion !== kibanaVersion) {
+        logger.debug(
+          `Stored telemetry is out of date. Task will run immediately. Stored: ${currentData.kibanaVersion}, expected: ${kibanaVersion}`
+        );
+        taskManagerStart.runNow(APM_TELEMETRY_TASK_NAME);
+      }
+    } catch (err) {
+      if (!SavedObjectsErrorHelpers.isNotFoundError(err)) {
+        logger.warn('Failed to fetch saved telemetry data.');
+      }
+    }
   });
 }
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts
index 82e4d1e395ed..c7af292e817c 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts
@@ -30,6 +30,11 @@ export type APMDataTelemetry = DeepPartial<{
       patch: number;
     };
   };
+  environments: {
+    services_without_environments: number;
+    services_with_multiple_environments: number;
+    top_environments: string[];
+  };
   aggregated_transactions: {
     current_implementation: AggregatedTransactionsCounts;
     no_observer_name: AggregatedTransactionsCounts;
diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
index 22b8c226e902..2cb28d378e8f 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
@@ -148,7 +148,7 @@ Object {
   "body": Object {
     "aggs": Object {
       "pageViews": Object {
-        "aggs": Object {},
+        "aggs": undefined,
         "auto_date_histogram": Object {
           "buckets": 50,
           "field": "@timestamp",
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts
index 23169ddaca53..114137e9fad1 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts
@@ -11,7 +11,6 @@ import {
   SetupTimeRange,
   SetupUIFilters,
 } from '../helpers/setup_request';
-import { AggregationInputMap } from '../../../typings/elasticsearch/aggregations';
 import { BreakdownItem } from '../../../typings/ui_filters';
 
 export async function getPageViewTrends({
@@ -24,18 +23,9 @@ export async function getPageViewTrends({
   const projection = getRumOverviewProjection({
     setup,
   });
-  const breakdownAggs: AggregationInputMap = {};
+  let breakdownItem: BreakdownItem | null = null;
   if (breakdowns) {
-    const breakdownList: BreakdownItem[] = JSON.parse(breakdowns);
-    breakdownList.forEach(({ name, type, fieldName }) => {
-      breakdownAggs[name] = {
-        terms: {
-          field: fieldName,
-          size: 9,
-          missing: 'Other',
-        },
-      };
-    });
+    breakdownItem = JSON.parse(breakdowns);
   }
 
   const params = mergeProjection(projection, {
@@ -50,7 +40,17 @@ export async function getPageViewTrends({
             field: '@timestamp',
             buckets: 50,
           },
-          aggs: breakdownAggs,
+          aggs: breakdownItem
+            ? {
+                breakdown: {
+                  terms: {
+                    field: breakdownItem.fieldName,
+                    size: 9,
+                    missing: 'Other',
+                  },
+                },
+              }
+            : undefined,
         },
       },
     },
@@ -68,19 +68,18 @@ export async function getPageViewTrends({
       x: xVal,
       y: bCount,
     };
-
-    Object.keys(breakdownAggs).forEach((bKey) => {
-      const categoryBuckets = (bucket[bKey] as any).buckets;
+    if (breakdownItem) {
+      const categoryBuckets = (bucket.breakdown as any).buckets;
       categoryBuckets.forEach(
         ({ key, doc_count: docCount }: { key: string; doc_count: number }) => {
           if (key === 'Other') {
-            res[key + `(${bKey})`] = docCount;
+            res[key + `(${breakdownItem?.name})`] = docCount;
           } else {
             res[key] = docCount;
           }
         }
       );
-    });
+    }
 
     return res;
   });
diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts
index d0ba31f42c53..5c1e1839d9c5 100644
--- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts
+++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts
@@ -185,10 +185,12 @@ export async function transactionGroupsFetcher(
 }
 
 export interface TransactionGroup {
-  key: Record<string, any> | string;
+  name?: string;
+  key?: Record<string, any> | string;
   averageResponseTime: number | null | undefined;
   transactionsPerMinute: number;
   p95: number | null | undefined;
   impact: number;
+  impactRelative?: number;
   sample: Transaction;
 }
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index 71202c62e6f6..f7e3977ae7d3 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -101,6 +101,7 @@ export class APMPlugin implements Plugin<APMPluginSetup> {
         usageCollector: plugins.usageCollection,
         taskManager: plugins.taskManager,
         logger: this.logger,
+        kibanaVersion: this.initContext.env.packageInfo.version,
       });
     }
 
diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
index 5e48f969c670..f95761412254 100644
--- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
+++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
@@ -51,7 +51,12 @@ type GetCompositeKeys<
 
 type CompositeOptionsSource = Record<
   string,
-  { terms: { field: string; missing_bucket?: boolean } } | undefined
+  | {
+      terms: ({ field: string } | { script: Script }) & {
+        missing_bucket?: boolean;
+      };
+    }
+  | undefined
 >;
 
 export interface AggregationOptionsByType {
@@ -281,10 +286,9 @@ interface AggregationResponsePart<
       }
     | undefined;
   composite: {
-    after_key: Record<
-      GetCompositeKeys<TAggregationOptionsMap>,
-      string | number
-    >;
+    after_key: {
+      [key in GetCompositeKeys<TAggregationOptionsMap>]: TAggregationOptionsMap;
+    };
     buckets: Array<
       {
         key: Record<GetCompositeKeys<TAggregationOptionsMap>, string | number>;
diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/host.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/host.ts
index 51c09e59d9b6..1869a4fc1bef 100644
--- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/host.ts
+++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/host.ts
@@ -5,5 +5,11 @@
  */
 
 export interface Host {
+  architecture?: string;
   hostname?: string;
+  name?: string;
+  ip?: string;
+  os?: {
+    platform?: string;
+  };
 }
diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts
index 0815b7cd8816..823d12cbd809 100644
--- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts
+++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/observer.ts
@@ -5,7 +5,11 @@
  */
 
 export interface Observer {
+  ephemeral_id?: string;
+  hostname?: string;
+  id?: string;
   name?: string;
+  type?: string;
   version: string;
   version_major: number;
 }
diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/process.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/process.ts
index 63e1faa38216..898ef04ed6a0 100644
--- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/process.ts
+++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/process.ts
@@ -5,8 +5,8 @@
  */
 
 export interface Process {
-  args: string[];
+  args?: string[];
   pid: number;
-  ppid: number;
-  title: string;
+  ppid?: number;
+  title?: string;
 }
diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/service.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/service.ts
index 3ef852ebf6dd..00795d69e13b 100644
--- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/service.ts
+++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/service.ts
@@ -9,7 +9,10 @@ export interface Service {
   environment?: string;
   framework?: {
     name: string;
-    version: string;
+    version?: string;
+  };
+  node?: {
+    name?: string;
   };
   runtime?: {
     name: string;
@@ -19,4 +22,5 @@ export interface Service {
     name: string;
     version?: string;
   };
+  version?: string;
 }
diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts
index b8ebb4cf8da5..cdfe4183c96f 100644
--- a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts
+++ b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts
@@ -54,6 +54,7 @@ export interface TransactionRaw extends APMBaseDoc {
 
   // Shared by errors and transactions
   container?: Container;
+  ecs?: { version?: string };
   host?: Host;
   http?: Http;
   kubernetes?: Kubernetes;
diff --git a/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts b/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts
index 1e542dec06a7..4d98825f36b5 100644
--- a/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts
+++ b/x-pack/plugins/apm/typings/es_schemas/ui/fields/agent.ts
@@ -19,6 +19,7 @@ export type AgentName =
   | 'ruby';
 
 export interface Agent {
+  ephemeral_id?: string;
   name: AgentName;
   version: string;
 }
diff --git a/x-pack/plugins/apm/typings/ui_filters.ts b/x-pack/plugins/apm/typings/ui_filters.ts
index 2a727dda7241..efba6919778b 100644
--- a/x-pack/plugins/apm/typings/ui_filters.ts
+++ b/x-pack/plugins/apm/typings/ui_filters.ts
@@ -14,7 +14,6 @@ export type UIFilters = {
 
 export interface BreakdownItem {
   name: string;
-  count: number;
   type: string;
   fieldName: string;
   selected?: boolean;
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx
index b5e5e248eaeb..607a11a76a06 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx
@@ -80,6 +80,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType<typeof OPEN_FLY
           viewMode={'create'}
           dynamicActionManager={embeddable.enhancements.dynamicActions}
           supportedTriggers={ensureNestedTriggers(embeddable.supportedTriggers())}
+          placeContext={{ embeddable }}
         />
       ),
       {
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx
index 6dfda93db715..30de62d0d28d 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx
@@ -64,6 +64,7 @@ export class FlyoutEditDrilldownAction implements ActionByType<typeof OPEN_FLYOU
           viewMode={'manage'}
           dynamicActionManager={embeddable.enhancements.dynamicActions}
           supportedTriggers={ensureNestedTriggers(embeddable.supportedTriggers())}
+          placeContext={{ embeddable }}
         />
       ),
       {
diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json
index 5663671de7bd..acada946fe0d 100644
--- a/x-pack/plugins/embeddable_enhanced/kibana.json
+++ b/x-pack/plugins/embeddable_enhanced/kibana.json
@@ -3,5 +3,6 @@
   "version": "kibana",
   "server": false,
   "ui": true,
-  "requiredPlugins": ["embeddable", "uiActionsEnhanced"]
+  "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"],
+  "requiredBundles": ["kibanaUtils"]
 }
diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts
similarity index 84%
rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts
rename to x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts
index 086ba6770312..a8d5a179dbac 100644
--- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts
@@ -4,4 +4,4 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-export declare const addAllExtensions: any;
+export * from './url_drilldown';
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md
new file mode 100644
index 000000000000..996723ccb914
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md
@@ -0,0 +1,24 @@
+# Basic url drilldown implementation
+
+Url drilldown allows navigating to external URL or to internal kibana URL.
+By using variables in url template result url can be dynamic and depend on user's interaction.
+
+URL drilldown has 3 sources for variables:
+
+- Global static variables like, for example, `kibanaUrl`. Such variables won’t change depending on a place where url drilldown is used.
+- Context variables are dynamic and different depending on where drilldown is created and used.
+- Event variables depend on a trigger context. These variables are dynamically extracted from the action context when drilldown is executed.
+
+Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel),
+but `event` variables mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL.
+
+In current implementation url drilldown has to be used inside the embeddable and with `ValueClickTrigger` or `RangeSelectTrigger`.
+
+- `context` variables extracted from `embeddable`
+- `event` variables extracted from `trigger` context
+
+In future this basic url drilldown implementation would allow injecting more variables into `context` (e.g. `dashboard` app specific variables) and would allow providing support for new trigger types from outside.
+This extensibility improvements are tracked here: https://github.com/elastic/kibana/issues/55324
+
+In case a solution app has a use case for url drilldown that has to be different from current basic implementation and
+just extending variables list is not enough, then recommendation is to create own custom url drilldown and reuse building blocks from `ui_actions_enhanced`.
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts
new file mode 100644
index 000000000000..748f6f4ceced
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const txtUrlDrilldownDisplayName = i18n.translate(
+  'xpack.embeddableEnhanced.drilldowns.urlDrilldownDisplayName',
+  {
+    defaultMessage: 'Go to URL',
+  }
+);
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts
new file mode 100644
index 000000000000..61406f7d8431
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { UrlDrilldown } from './url_drilldown';
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts
new file mode 100644
index 000000000000..6a11663ea6c3
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts
@@ -0,0 +1,166 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { UrlDrilldown, ActionContext, Config } from './url_drilldown';
+import { coreMock } from '../../../../../../src/core/public/mocks';
+import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public/lib/embeddables';
+
+const mockDataPoints = [
+  {
+    table: {
+      columns: [
+        {
+          name: 'test',
+          id: '1-1',
+          meta: {
+            type: 'histogram',
+            indexPatternId: 'logstash-*',
+            aggConfigParams: {
+              field: 'bytes',
+              interval: 30,
+              otherBucket: true,
+            },
+          },
+        },
+      ],
+      rows: [
+        {
+          '1-1': '2048',
+        },
+      ],
+    },
+    column: 0,
+    row: 0,
+    value: 'test',
+  },
+];
+
+const mockEmbeddable = ({
+  getInput: () => ({
+    filters: [],
+    timeRange: { from: 'now-15m', to: 'now' },
+    query: { query: 'test', language: 'kuery' },
+  }),
+  getOutput: () => ({}),
+} as unknown) as IEmbeddable;
+
+const mockNavigateToUrl = jest.fn(() => Promise.resolve());
+
+describe('UrlDrilldown', () => {
+  const urlDrilldown = new UrlDrilldown({
+    getGlobalScope: () => ({ kibanaUrl: 'http://localhost:5601/' }),
+    getOpenModal: () => Promise.resolve(coreMock.createStart().overlays.openModal),
+    getSyntaxHelpDocsLink: () => 'http://localhost:5601/docs',
+    navigateToUrl: mockNavigateToUrl,
+  });
+
+  test('license', () => {
+    expect(urlDrilldown.minimalLicense).toBe('gold');
+  });
+
+  describe('isCompatible', () => {
+    test('throws if no embeddable', async () => {
+      const config: Config = {
+        url: {
+          template: `https://elasti.co/?{{event.value}}`,
+        },
+        openInNewTab: false,
+      };
+
+      const context: ActionContext = {
+        data: {
+          data: mockDataPoints,
+        },
+      };
+
+      await expect(urlDrilldown.isCompatible(config, context)).rejects.toThrowError();
+    });
+
+    test('compatible if url is valid', async () => {
+      const config: Config = {
+        url: {
+          template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`,
+        },
+        openInNewTab: false,
+      };
+
+      const context: ActionContext = {
+        data: {
+          data: mockDataPoints,
+        },
+        embeddable: mockEmbeddable,
+      };
+
+      await expect(urlDrilldown.isCompatible(config, context)).resolves.toBe(true);
+    });
+
+    test('not compatible if url is invalid', async () => {
+      const config: Config = {
+        url: {
+          template: `https://elasti.co/?{{event.value}}&{{rison context.panel.somethingFake}}`,
+        },
+        openInNewTab: false,
+      };
+
+      const context: ActionContext = {
+        data: {
+          data: mockDataPoints,
+        },
+        embeddable: mockEmbeddable,
+      };
+
+      await expect(urlDrilldown.isCompatible(config, context)).resolves.toBe(false);
+    });
+  });
+
+  describe('getHref & execute', () => {
+    beforeEach(() => {
+      mockNavigateToUrl.mockReset();
+    });
+
+    test('valid url', async () => {
+      const config: Config = {
+        url: {
+          template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`,
+        },
+        openInNewTab: false,
+      };
+
+      const context: ActionContext = {
+        data: {
+          data: mockDataPoints,
+        },
+        embeddable: mockEmbeddable,
+      };
+
+      const url = await urlDrilldown.getHref(config, context);
+      expect(url).toMatchInlineSnapshot(`"https://elasti.co/?test&(language:kuery,query:test)"`);
+
+      await urlDrilldown.execute(config, context);
+      expect(mockNavigateToUrl).toBeCalledWith(url);
+    });
+
+    test('invalid url', async () => {
+      const config: Config = {
+        url: {
+          template: `https://elasti.co/?{{event.value}}&{{rison context.panel.invalid}}`,
+        },
+        openInNewTab: false,
+      };
+
+      const context: ActionContext = {
+        data: {
+          data: mockDataPoints,
+        },
+        embeddable: mockEmbeddable,
+      };
+
+      await expect(urlDrilldown.getHref(config, context)).rejects.toThrowError();
+      await expect(urlDrilldown.execute(config, context)).rejects.toThrowError();
+      expect(mockNavigateToUrl).not.toBeCalled();
+    });
+  });
+});
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx
new file mode 100644
index 000000000000..d5ab095fdd28
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx
@@ -0,0 +1,145 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { OverlayStart } from 'kibana/public';
+import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
+import { ChartActionContext, IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
+import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public';
+import {
+  SELECT_RANGE_TRIGGER,
+  VALUE_CLICK_TRIGGER,
+} from '../../../../../../src/plugins/ui_actions/public';
+import {
+  UiActionsEnhancedDrilldownDefinition as Drilldown,
+  UrlDrilldownGlobalScope,
+  UrlDrilldownConfig,
+  UrlDrilldownCollectConfig,
+  urlDrilldownValidateUrlTemplate,
+  urlDrilldownBuildScope,
+  urlDrilldownCompileUrl,
+  UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext,
+} from '../../../../ui_actions_enhanced/public';
+import { getContextScope, getEventScope, getMockEventScope } from './url_drilldown_scope';
+import { txtUrlDrilldownDisplayName } from './i18n';
+
+interface UrlDrilldownDeps {
+  getGlobalScope: () => UrlDrilldownGlobalScope;
+  navigateToUrl: (url: string) => Promise<void>;
+  getOpenModal: () => Promise<OverlayStart['openModal']>;
+  getSyntaxHelpDocsLink: () => string;
+}
+
+export type ActionContext = ChartActionContext;
+export type Config = UrlDrilldownConfig;
+export type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER;
+export interface ActionFactoryContext extends BaseActionFactoryContext<UrlTrigger> {
+  embeddable?: IEmbeddable;
+}
+export type CollectConfigProps = CollectConfigPropsBase<Config, ActionFactoryContext>;
+
+const URL_DRILLDOWN = 'URL_DRILLDOWN';
+
+export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactoryContext> {
+  public readonly id = URL_DRILLDOWN;
+
+  constructor(private deps: UrlDrilldownDeps) {}
+
+  public readonly order = 8;
+
+  readonly minimalLicense = 'gold';
+  readonly licenseFeatureName = 'URL drilldown';
+
+  public readonly getDisplayName = () => txtUrlDrilldownDisplayName;
+
+  public readonly euiIcon = 'link';
+
+  supportedTriggers(): UrlTrigger[] {
+    return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER];
+  }
+
+  private readonly ReactCollectConfig: React.FC<CollectConfigProps> = ({
+    config,
+    onConfig,
+    context,
+  }) => {
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    const scope = React.useMemo(() => this.buildEditorScope(context), [context]);
+    return (
+      <UrlDrilldownCollectConfig
+        config={config}
+        onConfig={onConfig}
+        scope={scope}
+        syntaxHelpDocsLink={this.deps.getSyntaxHelpDocsLink()}
+      />
+    );
+  };
+
+  public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig);
+
+  public readonly createConfig = () => ({
+    url: { template: '' },
+    openInNewTab: false,
+  });
+
+  public readonly isConfigValid = (
+    config: Config,
+    context: ActionFactoryContext
+  ): config is Config => {
+    const { isValid } = urlDrilldownValidateUrlTemplate(config.url, this.buildEditorScope(context));
+    return isValid;
+  };
+
+  public readonly isCompatible = async (config: Config, context: ActionContext) => {
+    const { isValid, error } = urlDrilldownValidateUrlTemplate(
+      config.url,
+      await this.buildRuntimeScope(context)
+    );
+
+    if (!isValid) {
+      // eslint-disable-next-line no-console
+      console.warn(
+        `UrlDrilldown [${config.url.template}] is not valid. Error [${error}]. Skipping execution.`
+      );
+    }
+
+    return Promise.resolve(isValid);
+  };
+
+  public readonly getHref = async (config: Config, context: ActionContext) =>
+    urlDrilldownCompileUrl(config.url.template, await this.buildRuntimeScope(context));
+
+  public readonly execute = async (config: Config, context: ActionContext) => {
+    const url = await urlDrilldownCompileUrl(
+      config.url.template,
+      await this.buildRuntimeScope(context, { allowPrompts: true })
+    );
+    if (config.openInNewTab) {
+      window.open(url, '_blank', 'noopener');
+    } else {
+      await this.deps.navigateToUrl(url);
+    }
+  };
+
+  private buildEditorScope = (context: ActionFactoryContext) => {
+    return urlDrilldownBuildScope({
+      globalScope: this.deps.getGlobalScope(),
+      contextScope: getContextScope(context),
+      eventScope: getMockEventScope(context.triggers),
+    });
+  };
+
+  private buildRuntimeScope = async (
+    context: ActionContext,
+    opts: { allowPrompts: boolean } = { allowPrompts: false }
+  ) => {
+    return urlDrilldownBuildScope({
+      globalScope: this.deps.getGlobalScope(),
+      contextScope: getContextScope(context),
+      eventScope: await getEventScope(context, this.deps, opts),
+    });
+  };
+}
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx
new file mode 100644
index 000000000000..d3e3510f1b24
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx
@@ -0,0 +1,319 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/**
+ * This file contains all the logic for mapping from trigger's context and action factory context to variables for URL drilldown scope,
+ * Please refer to ./README.md for explanation of different scope sources
+ */
+
+import React from 'react';
+import {
+  EuiButton,
+  EuiButtonEmpty,
+  EuiModalBody,
+  EuiModalFooter,
+  EuiModalHeader,
+  EuiModalHeaderTitle,
+  EuiRadioGroup,
+} from '@elastic/eui';
+import uniqBy from 'lodash/uniqBy';
+import { FormattedMessage } from '@kbn/i18n/react';
+import type { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/public';
+import {
+  IEmbeddable,
+  isRangeSelectTriggerContext,
+  isValueClickTriggerContext,
+  RangeSelectContext,
+  ValueClickContext,
+} from '../../../../../../src/plugins/embeddable/public';
+import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown';
+import { SELECT_RANGE_TRIGGER } from '../../../../../../src/plugins/ui_actions/public';
+import { OverlayStart } from '../../../../../../src/core/public';
+import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
+
+type ContextScopeInput = ActionContext | ActionFactoryContext;
+
+/**
+ * Part of context scope extracted from an embeddable
+ * Expose on the scope as: `{{context.panel.id}}`, `{{context.panel.filters.[0]}}`
+ */
+interface EmbeddableUrlDrilldownContextScope {
+  id: string;
+  title?: string;
+  query?: Query;
+  filters?: Filter[];
+  timeRange?: TimeRange;
+  savedObjectId?: string;
+  /**
+   * In case panel supports only 1 index patterns
+   */
+  indexPatternId?: string;
+  /**
+   * In case panel supports more then 1 index patterns
+   */
+  indexPatternIds?: string[];
+}
+
+/**
+ * Url drilldown context scope
+ * `{{context.$}}`
+ */
+interface UrlDrilldownContextScope {
+  panel?: EmbeddableUrlDrilldownContextScope;
+}
+
+export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilldownContextScope {
+  function hasEmbeddable(val: unknown): val is { embeddable: IEmbeddable } {
+    if (val && typeof val === 'object' && 'embeddable' in val) return true;
+    return false;
+  }
+  if (!hasEmbeddable(contextScopeInput))
+    throw new Error(
+      "UrlDrilldown [getContextScope] can't build scope because embeddable object is missing in context"
+    );
+
+  const embeddable = contextScopeInput.embeddable;
+  const input = embeddable.getInput();
+  const output = embeddable.getOutput();
+  function hasSavedObjectId(obj: Record<string, any>): obj is { savedObjectId: string } {
+    return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string';
+  }
+  function getIndexPatternIds(): string[] {
+    function hasIndexPatterns(
+      _output: Record<string, any>
+    ): _output is { indexPatterns: Array<{ id?: string }> } {
+      return (
+        'indexPatterns' in _output &&
+        Array.isArray(_output.indexPatterns) &&
+        _output.indexPatterns.length > 0
+      );
+    }
+    return hasIndexPatterns(output)
+      ? (output.indexPatterns.map((ip) => ip.id).filter(Boolean) as string[])
+      : [];
+  }
+  const indexPatternsIds = getIndexPatternIds();
+  return {
+    panel: cleanEmptyKeys({
+      id: input.id,
+      title: output.title ?? input.title,
+      savedObjectId:
+        output.savedObjectId ?? (hasSavedObjectId(input) ? input.savedObjectId : undefined),
+      query: input.query,
+      timeRange: input.timeRange,
+      filters: input.filters,
+      indexPatternIds: indexPatternsIds.length > 1 ? indexPatternsIds : undefined,
+      indexPatternId: indexPatternsIds.length === 1 ? indexPatternsIds[0] : undefined,
+    }),
+  };
+}
+
+/**
+ * URL drilldown event scope,
+ * available as: {{event.key}}, {{event.from}}
+ */
+type UrlDrilldownEventScope = ValueClickTriggerEventScope | RangeSelectTriggerEventScope;
+type EventScopeInput = ActionContext;
+interface ValueClickTriggerEventScope {
+  key?: string;
+  value?: string | number | boolean;
+  negate: boolean;
+}
+interface RangeSelectTriggerEventScope {
+  key: string;
+  from?: string | number;
+  to?: string | number;
+}
+
+export async function getEventScope(
+  eventScopeInput: EventScopeInput,
+  deps: { getOpenModal: () => Promise<OverlayStart['openModal']> },
+  opts: { allowPrompts: boolean } = { allowPrompts: false }
+): Promise<UrlDrilldownEventScope> {
+  if (isRangeSelectTriggerContext(eventScopeInput)) {
+    return getEventScopeFromRangeSelectTriggerContext(eventScopeInput);
+  } else if (isValueClickTriggerContext(eventScopeInput)) {
+    return getEventScopeFromValueClickTriggerContext(eventScopeInput, deps, opts);
+  } else {
+    throw new Error("UrlDrilldown [getEventScope] can't build scope from not supported trigger");
+  }
+}
+
+async function getEventScopeFromRangeSelectTriggerContext(
+  eventScopeInput: RangeSelectContext
+): Promise<RangeSelectTriggerEventScope> {
+  const { table, column: columnIndex, range } = eventScopeInput.data;
+  const column = table.columns[columnIndex];
+  return cleanEmptyKeys({
+    key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string,
+    from: toPrimitiveOrUndefined(range[0]) as string | number | undefined,
+    to: toPrimitiveOrUndefined(range[range.length - 1]) as string | number | undefined,
+  });
+}
+
+async function getEventScopeFromValueClickTriggerContext(
+  eventScopeInput: ValueClickContext,
+  deps: { getOpenModal: () => Promise<OverlayStart['openModal']> },
+  opts: { allowPrompts: boolean } = { allowPrompts: false }
+): Promise<ValueClickTriggerEventScope> {
+  const negate = eventScopeInput.data.negate ?? false;
+  const point = await getSingleValue(eventScopeInput.data.data, deps, opts);
+  const { key, value } = getKeyValueFromPoint(point);
+  return cleanEmptyKeys({
+    key,
+    value,
+    negate,
+  });
+}
+
+/**
+ * @remarks
+ * Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel)
+ * `event` variables are mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL
+ */
+export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventScope {
+  if (trigger === SELECT_RANGE_TRIGGER) {
+    return {
+      key: 'event.key',
+      from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), // 15 minutes ago
+      to: new Date().toISOString(),
+    };
+  } else {
+    return {
+      key: 'event.key',
+      value: 'event.value',
+      negate: false,
+    };
+  }
+}
+
+function getKeyValueFromPoint(
+  point: ValueClickContext['data']['data'][0]
+): Pick<ValueClickTriggerEventScope, 'key' | 'value'> {
+  const { table, column: columnIndex, value } = point;
+  const column = table.columns[columnIndex];
+  return {
+    key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string | undefined,
+    value: toPrimitiveOrUndefined(value),
+  };
+}
+
+function toPrimitiveOrUndefined(v: unknown): string | number | boolean | undefined {
+  if (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'string') return v;
+  if (typeof v === 'object' && v instanceof Date) return v.toISOString();
+  if (typeof v === 'undefined' || v === null) return undefined;
+  return String(v);
+}
+
+function cleanEmptyKeys<T extends Record<string, any>>(obj: T): T {
+  Object.keys(obj).forEach((key) => {
+    if (obj[key] === undefined) {
+      delete obj[key];
+    }
+  });
+  return obj;
+}
+
+/**
+ * VALUE_CLICK_TRIGGER could have multiple data points
+ * Prompt user which data point to use in a drilldown
+ */
+async function getSingleValue(
+  data: ValueClickContext['data']['data'],
+  deps: { getOpenModal: () => Promise<OverlayStart['openModal']> },
+  opts: { allowPrompts: boolean } = { allowPrompts: false }
+): Promise<ValueClickContext['data']['data'][0]> {
+  data = uniqBy(data.filter(Boolean), (point) => {
+    const { key, value } = getKeyValueFromPoint(point);
+    return `${key}:${value}`;
+  });
+  if (data.length === 0)
+    throw new Error(`[trigger = "VALUE_CLICK_TRIGGER"][getSingleValue] no value to pick from`);
+  if (data.length === 1) return Promise.resolve(data[0]);
+  if (!opts.allowPrompts) return Promise.resolve(data[0]);
+  return new Promise(async (resolve, reject) => {
+    const openModal = await deps.getOpenModal();
+    const overlay = openModal(
+      toMountPoint(
+        <GetSingleValuePopup
+          onCancel={() => overlay.close()}
+          onSubmit={(point) => {
+            if (point) {
+              resolve(point);
+            }
+            overlay.close();
+          }}
+          data={data}
+        />
+      )
+    );
+    overlay.onClose.then(() => reject());
+  });
+}
+
+function GetSingleValuePopup({
+  data,
+  onCancel,
+  onSubmit,
+}: {
+  data: ValueClickContext['data']['data'];
+  onCancel: () => void;
+  onSubmit: (value: ValueClickContext['data']['data'][0]) => void;
+}) {
+  const values = data
+    .map((point) => {
+      const { key, value } = getKeyValueFromPoint(point);
+      return {
+        point,
+        id: key ?? '',
+        label: `${key}:${value}`,
+      };
+    })
+    .filter((value) => Boolean(value.id));
+
+  const [selectedValueId, setSelectedValueId] = React.useState(values[0].id);
+
+  return (
+    <React.Fragment>
+      <EuiModalHeader>
+        <EuiModalHeaderTitle>
+          <FormattedMessage
+            id="xpack.embeddableEnhanced.drilldowns.pickSingleValuePopup.popupHeader"
+            defaultMessage="Select a value to drill down into"
+          />
+        </EuiModalHeaderTitle>
+      </EuiModalHeader>
+
+      <EuiModalBody>
+        <EuiRadioGroup
+          options={values}
+          idSelected={selectedValueId}
+          onChange={(id) => setSelectedValueId(id)}
+          name="drilldownValues"
+        />
+      </EuiModalBody>
+
+      <EuiModalFooter>
+        <EuiButtonEmpty onClick={onCancel}>
+          <FormattedMessage
+            id="xpack.embeddableEnhanced.drilldowns.pickSingleValuePopup.cancelButtonLabel"
+            defaultMessage="Cancel"
+          />
+        </EuiButtonEmpty>
+        <EuiButton
+          onClick={() => onSubmit(values.find((v) => v.id === selectedValueId)?.point!)}
+          data-test-subj="applySingleValuePopoverButton"
+          fill
+        >
+          <FormattedMessage
+            id="xpack.embeddableEnhanced.drilldowns.pickSingleValuePopup.applyButtonLabel"
+            defaultMessage="Apply"
+          />
+        </EuiButton>
+      </EuiModalFooter>
+    </React.Fragment>
+  );
+}
diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts
index fd0bcc202326..37e102b40131 100644
--- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts
+++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts
@@ -28,8 +28,11 @@ import {
   UiActionsEnhancedDynamicActionManager as DynamicActionManager,
   AdvancedUiActionsSetup,
   AdvancedUiActionsStart,
+  urlDrilldownGlobalScopeProvider,
 } from '../../ui_actions_enhanced/public';
 import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions';
+import { UrlDrilldown } from './drilldowns';
+import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public';
 
 declare module '../../../../src/plugins/ui_actions/public' {
   export interface ActionContextMapping {
@@ -61,11 +64,21 @@ export class EmbeddableEnhancedPlugin
 
   public setup(core: CoreSetup<StartDependencies>, plugins: SetupDependencies): SetupContract {
     this.setCustomEmbeddableFactoryProvider(plugins);
-
+    const startServices = createStartServicesGetter(core.getStartServices);
     const panelNotificationAction = new PanelNotificationsAction();
     plugins.uiActionsEnhanced.registerAction(panelNotificationAction);
     plugins.uiActionsEnhanced.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id);
 
+    plugins.uiActionsEnhanced.registerDrilldown(
+      new UrlDrilldown({
+        getGlobalScope: urlDrilldownGlobalScopeProvider({ core }),
+        navigateToUrl: (url: string) =>
+          core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)),
+        getOpenModal: () => core.getStartServices().then(([{ overlays }]) => overlays.openModal),
+        getSyntaxHelpDocsLink: () => startServices().core.docLinks.links.dashboard.drilldowns, // TODO: replace with docs https://github.com/elastic/kibana/issues/69414
+      })
+    );
+
     return {};
   }
 
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
similarity index 89%
rename from x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap
rename to x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
index 39eb54b941ac..59d1723c3948 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap
@@ -134,6 +134,7 @@ exports[`extend index management ilm summary extension should return extension w
         },
         "step_time_millis": 1544187776208,
       },
+      "isFrozen": false,
       "name": "testy3",
       "primary": "1",
       "primary_size": "6.5kb",
@@ -326,6 +327,82 @@ exports[`extend index management ilm summary extension should return extension w
               className="euiSpacer euiSpacer--s"
             />
           </EuiSpacer>
+          <EuiPopover
+            anchorPosition="downCenter"
+            button={
+              <EuiButtonEmpty
+                onClick={[Function]}
+              >
+                <FormattedMessage
+                  defaultMessage="Stack trace"
+                  id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.stackTraceButton"
+                  values={Object {}}
+                />
+              </EuiButtonEmpty>
+            }
+            closePopover={[Function]}
+            display="inlineBlock"
+            hasArrow={true}
+            id="stackPopover"
+            isOpen={false}
+            ownFocus={false}
+            panelPaddingSize="m"
+          >
+            <EuiOutsideClickDetector
+              isDisabled={true}
+              onOutsideClick={[Function]}
+            >
+              <div
+                className="euiPopover euiPopover--anchorDownCenter"
+                id="stackPopover"
+                onKeyDown={[Function]}
+                onMouseDown={[Function]}
+                onMouseUp={[Function]}
+                onTouchEnd={[Function]}
+                onTouchStart={[Function]}
+              >
+                <div
+                  className="euiPopover__anchor"
+                >
+                  <EuiButtonEmpty
+                    onClick={[Function]}
+                  >
+                    <button
+                      className="euiButtonEmpty euiButtonEmpty--primary"
+                      onClick={[Function]}
+                      type="button"
+                    >
+                      <EuiButtonContent
+                        className="euiButtonEmpty__content"
+                        iconSide="left"
+                        textProps={
+                          Object {
+                            "className": "euiButtonEmpty__text",
+                          }
+                        }
+                      >
+                        <span
+                          className="euiButtonContent euiButtonEmpty__content"
+                        >
+                          <span
+                            className="euiButtonEmpty__text"
+                          >
+                            <FormattedMessage
+                              defaultMessage="Stack trace"
+                              id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.stackTraceButton"
+                              values={Object {}}
+                            >
+                              Stack trace
+                            </FormattedMessage>
+                          </span>
+                        </span>
+                      </EuiButtonContent>
+                    </button>
+                  </EuiButtonEmpty>
+                </div>
+              </div>
+            </EuiOutsideClickDetector>
+          </EuiPopover>
         </div>
       </EuiText>
     </div>
@@ -588,6 +665,7 @@ exports[`extend index management ilm summary extension should return extension w
         "step": "complete",
         "step_time_millis": 1544187775867,
       },
+      "isFrozen": false,
       "name": "testy3",
       "primary": "1",
       "primary_size": "6.5kb",
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx
similarity index 88%
rename from x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js
rename to x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx
index 4fa183811584..17573cb81c40 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx
@@ -8,7 +8,8 @@ import moment from 'moment-timezone';
 import axios from 'axios';
 import axiosXhrAdapter from 'axios/lib/adapters/xhr';
 
-import { mountWithIntl } from '../../../test_utils/enzyme_helpers';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/public/mocks';
 import {
   retryLifecycleActionExtension,
   removeLifecyclePolicyActionExtension,
@@ -19,19 +20,22 @@ import {
 } from '../public/extend_index_management';
 import { init as initHttp } from '../public/application/services/http';
 import { init as initUiMetric } from '../public/application/services/ui_metric';
+import { Index } from '../public/application/services/policies/types';
 
 // We need to init the http with a mock for any tests that depend upon the http service.
 // For example, add_lifecycle_confirm_modal makes an API request in its componentDidMount
 // lifecycle method. If we don't mock this, CI will fail with "Call retries were exceeded".
-initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path);
-initUiMetric({ reportUiStats: () => {} });
+// This expects HttpSetup but we're giving it AxiosInstance.
+// @ts-ignore
+initHttp(axios.create({ adapter: axiosXhrAdapter }));
+initUiMetric(usageCollectionPluginMock.createSetupContract());
 
 jest.mock('../../../plugins/index_management/public', async () => {
-  const { indexManagementMock } = await import('../../../plugins/index_management/public/mocks.ts');
+  const { indexManagementMock } = await import('../../../plugins/index_management/public/mocks');
   return indexManagementMock.createSetup();
 });
 
-const indexWithoutLifecyclePolicy = {
+const indexWithoutLifecyclePolicy: Index = {
   health: 'yellow',
   status: 'open',
   name: 'noPolicy',
@@ -43,13 +47,14 @@ const indexWithoutLifecyclePolicy = {
   size: '3.4kb',
   primary_size: '3.4kb',
   aliases: 'none',
+  isFrozen: false,
   ilm: {
     index: 'testy1',
     managed: false,
   },
 };
 
-const indexWithLifecyclePolicy = {
+const indexWithLifecyclePolicy: Index = {
   health: 'yellow',
   status: 'open',
   name: 'testy3',
@@ -61,6 +66,7 @@ const indexWithLifecyclePolicy = {
   size: '6.5kb',
   primary_size: '6.5kb',
   aliases: 'none',
+  isFrozen: false,
   ilm: {
     index: 'testy3',
     managed: true,
@@ -87,6 +93,7 @@ const indexWithLifecycleError = {
   size: '6.5kb',
   primary_size: '6.5kb',
   aliases: 'none',
+  isFrozen: false,
   ilm: {
     index: 'testy3',
     managed: true,
@@ -115,10 +122,12 @@ const indexWithLifecycleError = {
 
 moment.tz.setDefault('utc');
 
-const getUrlForApp = (appId, options) => {
+const getUrlForApp = (appId: string, options: any) => {
   return appId + '/' + (options ? options.path : '');
 };
 
+const reloadIndices = () => {};
+
 describe('extend index management', () => {
   describe('retry lifecycle action extension', () => {
     test('should return null when no indices have index lifecycle policy', () => {
@@ -153,6 +162,7 @@ describe('extend index management', () => {
     test('should return null when no indices have index lifecycle policy', () => {
       const extension = removeLifecyclePolicyActionExtension({
         indices: [indexWithoutLifecyclePolicy],
+        reloadIndices,
       });
       expect(extension).toBeNull();
     });
@@ -160,6 +170,7 @@ describe('extend index management', () => {
     test('should return null when some indices have index lifecycle policy', () => {
       const extension = removeLifecyclePolicyActionExtension({
         indices: [indexWithoutLifecyclePolicy, indexWithLifecyclePolicy],
+        reloadIndices,
       });
       expect(extension).toBeNull();
     });
@@ -167,6 +178,7 @@ describe('extend index management', () => {
     test('should return extension when all indices have lifecycle policy', () => {
       const extension = removeLifecyclePolicyActionExtension({
         indices: [indexWithLifecycleError, indexWithLifecycleError],
+        reloadIndices,
       });
       expect(extension).toBeDefined();
       expect(extension).toMatchSnapshot();
@@ -175,16 +187,18 @@ describe('extend index management', () => {
 
   describe('add lifecycle policy action extension', () => {
     test('should return null when index has index lifecycle policy', () => {
-      const extension = addLifecyclePolicyActionExtension(
-        { indices: [indexWithLifecyclePolicy] },
-        getUrlForApp
-      );
+      const extension = addLifecyclePolicyActionExtension({
+        indices: [indexWithLifecyclePolicy],
+        reloadIndices,
+        getUrlForApp,
+      });
       expect(extension).toBeNull();
     });
 
     test('should return null when more than one index is passed', () => {
       const extension = addLifecyclePolicyActionExtension({
         indices: [indexWithoutLifecyclePolicy, indexWithoutLifecyclePolicy],
+        reloadIndices,
         getUrlForApp,
       });
       expect(extension).toBeNull();
@@ -193,10 +207,11 @@ describe('extend index management', () => {
     test('should return extension when one index is passed and it does not have lifecycle policy', () => {
       const extension = addLifecyclePolicyActionExtension({
         indices: [indexWithoutLifecyclePolicy],
+        reloadIndices,
         getUrlForApp,
       });
-      expect(extension.renderConfirmModal).toBeDefined;
-      const component = extension.renderConfirmModal(jest.fn());
+      expect(extension?.renderConfirmModal).toBeDefined();
+      const component = extension!.renderConfirmModal(jest.fn());
       const rendered = mountWithIntl(component);
       expect(rendered.exists('.euiModal--confirmation'));
     });
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts
index b80e9e70c54f..e9365bfe06ea 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts
@@ -12,7 +12,7 @@ import {
   UIM_POLICY_ATTACH_INDEX_TEMPLATE,
   UIM_POLICY_DETACH_INDEX,
   UIM_INDEX_RETRY_STEP,
-} from '../constants/ui_metric';
+} from '../constants';
 
 import { trackUiMetric } from './ui_metric';
 import { sendGet, sendPost, sendDelete, useRequest } from './http';
@@ -78,7 +78,11 @@ export const removeLifecycleForIndex = async (indexNames: string[]) => {
   return response;
 };
 
-export const addLifecyclePolicyToIndex = async (body: GenericObject) => {
+export const addLifecyclePolicyToIndex = async (body: {
+  indexName: string;
+  policyName: string;
+  alias: string;
+}) => {
   const response = await sendPost(`index/add`, body);
   // Only track successful actions.
   trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts
index c191f82cf05c..0e00b5a02b71 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts
@@ -4,6 +4,8 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
+import { Index as IndexInterface } from '../../../../../index_management/public';
+
 export interface SerializedPolicy {
   name: string;
   phases: Phases;
@@ -169,3 +171,36 @@ export interface FrozenPhase
 export interface DeletePhase extends CommonPhaseSettings, PhaseWithMinAge {
   waitForSnapshotPolicy: string;
 }
+
+export interface IndexLifecyclePolicy {
+  index: string;
+  managed: boolean;
+  action?: string;
+  action_time_millis?: number;
+  age?: string;
+  failed_step?: string;
+  failed_step_retry_count?: number;
+  is_auto_retryable_error?: boolean;
+  lifecycle_date_millis?: number;
+  phase?: string;
+  phase_execution?: {
+    policy: string;
+    modified_date_in_millis: number;
+    version: number;
+    phase_definition: SerializedPhase;
+  };
+  phase_time_millis?: number;
+  policy?: string;
+  step?: string;
+  step_info?: {
+    reason?: string;
+    stack_trace?: string;
+    type?: string;
+    message?: string;
+  };
+  step_time_millis?: number;
+}
+
+export interface Index extends IndexInterface {
+  ilm: IndexLifecyclePolicy;
+}
diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx
similarity index 88%
rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js
rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx
index 0bd313c9a9f8..060b208006bf 100644
--- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js
+++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx
@@ -8,6 +8,8 @@ import React, { Component, Fragment } from 'react';
 import { get } from 'lodash';
 import { i18n } from '@kbn/i18n';
 import { FormattedMessage } from '@kbn/i18n/react';
+import { ApplicationStart } from 'kibana/public';
+
 import {
   EuiLink,
   EuiSelect,
@@ -26,9 +28,25 @@ import {
 import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api';
 import { showApiError } from '../../application/services/api_errors';
 import { toasts } from '../../application/services/notification';
+import { Index, PolicyFromES } from '../../application/services/policies/types';
+
+interface Props {
+  indexName: string;
+  closeModal: () => void;
+  index: Index;
+  reloadIndices: () => void;
+  getUrlForApp: ApplicationStart['getUrlForApp'];
+}
+
+interface State {
+  selectedPolicyName: string;
+  selectedAlias: string;
+  policies: PolicyFromES[];
+  policyErrorMessage?: string;
+}
 
-export class AddLifecyclePolicyConfirmModal extends Component {
-  constructor(props) {
+export class AddLifecyclePolicyConfirmModal extends Component<Props, State> {
+  constructor(props: Props) {
     super(props);
     this.state = {
       policies: [],
@@ -41,7 +59,7 @@ export class AddLifecyclePolicyConfirmModal extends Component {
     const { selectedPolicyName, selectedAlias } = this.state;
     if (!selectedPolicyName) {
       this.setState({
-        policyError: i18n.translate(
+        policyErrorMessage: i18n.translate(
           'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.noPolicySelectedErrorMessage',
           { defaultMessage: 'You must select a policy.' }
         ),
@@ -81,7 +99,7 @@ export class AddLifecyclePolicyConfirmModal extends Component {
       );
     }
   };
-  renderAliasFormElement = (selectedPolicy) => {
+  renderAliasFormElement = (selectedPolicy?: PolicyFromES) => {
     const { selectedAlias } = this.state;
     const { index } = this.props;
     const showAliasSelect =
@@ -109,7 +127,7 @@ export class AddLifecyclePolicyConfirmModal extends Component {
               defaultMessage="Policy {policyName} is configured for rollover,
                 but index {indexName} does not have an alias, which is required for rollover."
               values={{
-                policyName: selectedPolicy.name,
+                policyName: selectedPolicy?.name,
                 indexName: index.name,
               }}
             />
@@ -117,7 +135,7 @@ export class AddLifecyclePolicyConfirmModal extends Component {
         </Fragment>
       );
     }
-    const aliasOptions = aliases.map((alias) => {
+    const aliasOptions = (aliases as string[]).map((alias: string) => {
       return {
         text: alias,
         value: alias,
@@ -152,10 +170,10 @@ export class AddLifecyclePolicyConfirmModal extends Component {
     );
   };
   renderForm() {
-    const { policies, selectedPolicyName, policyError } = this.state;
+    const { policies, selectedPolicyName, policyErrorMessage } = this.state;
     const selectedPolicy = selectedPolicyName
       ? policies.find((policy) => policy.name === selectedPolicyName)
-      : null;
+      : undefined;
 
     const options = policies.map(({ name }) => {
       return {
@@ -175,8 +193,8 @@ export class AddLifecyclePolicyConfirmModal extends Component {
     return (
       <EuiForm>
         <EuiFormRow
-          isInvalid={!!policyError}
-          error={policyError}
+          isInvalid={!!policyErrorMessage}
+          error={policyErrorMessage}
           label={
             <FormattedMessage
               id="xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.choosePolicyLabel"
@@ -188,7 +206,7 @@ export class AddLifecyclePolicyConfirmModal extends Component {
             options={options}
             value={selectedPolicyName}
             onChange={(e) => {
-              this.setState({ policyError: null, selectedPolicyName: e.target.value });
+              this.setState({ policyErrorMessage: undefined, selectedPolicyName: e.target.value });
             }}
           />
         </EuiFormRow>
@@ -198,7 +216,7 @@ export class AddLifecyclePolicyConfirmModal extends Component {
   }
   async componentDidMount() {
     try {
-      const policies = await loadPolicies(false, this.props.httpClient);
+      const policies = await loadPolicies(false);
       this.setState({ policies });
     } catch (err) {
       showApiError(
diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx
similarity index 69%
rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js
rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx
index 9e9dc009e4c4..02e4595a333b 100644
--- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js
+++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx
@@ -24,44 +24,71 @@ import {
   EuiPopoverTitle,
 } from '@elastic/eui';
 
+import { ApplicationStart } from 'kibana/public';
 import { getPolicyPath } from '../../application/services/navigation';
+import { Index, IndexLifecyclePolicy } from '../../application/services/policies/types';
 
-const getHeaders = () => {
-  return {
-    policy: i18n.translate(
-      'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.lifecyclePolicyHeader',
-      {
-        defaultMessage: 'Lifecycle policy',
-      }
-    ),
-    phase: i18n.translate(
-      'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentPhaseHeader',
-      {
-        defaultMessage: 'Current phase',
-      }
-    ),
-    action: i18n.translate(
-      'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionHeader',
-      {
-        defaultMessage: 'Current action',
-      }
-    ),
-    action_time_millis: i18n.translate(
-      'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionTimeHeader',
-      {
-        defaultMessage: 'Current action time',
-      }
-    ),
-    failed_step: i18n.translate(
-      'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.failedStepHeader',
-      {
-        defaultMessage: 'Failed step',
-      }
-    ),
-  };
+const getHeaders = (): Array<[keyof IndexLifecyclePolicy, string]> => {
+  return [
+    [
+      'policy',
+      i18n.translate(
+        'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.lifecyclePolicyHeader',
+        {
+          defaultMessage: 'Lifecycle policy',
+        }
+      ),
+    ],
+    [
+      'phase',
+      i18n.translate(
+        'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentPhaseHeader',
+        {
+          defaultMessage: 'Current phase',
+        }
+      ),
+    ],
+    [
+      'action',
+      i18n.translate(
+        'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionHeader',
+        {
+          defaultMessage: 'Current action',
+        }
+      ),
+    ],
+    [
+      'action_time_millis',
+      i18n.translate(
+        'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionTimeHeader',
+        {
+          defaultMessage: 'Current action time',
+        }
+      ),
+    ],
+    [
+      'failed_step',
+      i18n.translate(
+        'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.failedStepHeader',
+        {
+          defaultMessage: 'Failed step',
+        }
+      ),
+    ],
+  ];
 };
-export class IndexLifecycleSummary extends Component {
-  constructor(props) {
+
+interface Props {
+  index: Index;
+  getUrlForApp: ApplicationStart['getUrlForApp'];
+}
+interface State {
+  showStackPopover: boolean;
+  showPhaseExecutionPopover: boolean;
+}
+
+export class IndexLifecycleSummary extends Component<Props, State> {
+  constructor(props: Props) {
     super(props);
     this.state = {
       showStackPopover: false,
@@ -80,8 +107,8 @@ export class IndexLifecycleSummary extends Component {
   closePhaseExecutionPopover = () => {
     this.setState({ showPhaseExecutionPopover: false });
   };
-  renderStackPopoverButton(ilm) {
-    if (!ilm.stack_trace) {
+  renderStackPopoverButton(ilm: IndexLifecyclePolicy) {
+    if (!ilm.step_info!.stack_trace) {
       return null;
     }
     const button = (
@@ -100,15 +127,12 @@ export class IndexLifecycleSummary extends Component {
         closePopover={this.closeStackPopover}
       >
         <div style={{ maxHeight: '400px', width: '900px', overflowY: 'scroll' }}>
-          <pre>{ilm.step_info.stack_trace}</pre>
+          <pre>{ilm.step_info!.stack_trace}</pre>
         </div>
       </EuiPopover>
     );
   }
-  renderPhaseExecutionPopoverButton(ilm) {
-    if (!ilm.phase_execution) {
-      return null;
-    }
+  renderPhaseExecutionPopoverButton(ilm: IndexLifecyclePolicy) {
     const button = (
       <EuiLink onClick={this.togglePhaseExecutionPopover}>
         <FormattedMessage
@@ -150,15 +174,18 @@ export class IndexLifecycleSummary extends Component {
   }
   buildRows() {
     const {
-      index: { ilm = {} },
+      index: { ilm },
     } = this.props;
     const headers = getHeaders();
-    const rows = {
+    const rows: {
+      left: JSX.Element[];
+      right: JSX.Element[];
+    } = {
       left: [],
       right: [],
     };
-    Object.keys(headers).forEach((fieldName, arrayIndex) => {
-      const value = ilm[fieldName];
+    headers.forEach(([fieldName, label], arrayIndex) => {
+      const value: any = ilm[fieldName];
       let content;
       if (fieldName === 'action_time_millis') {
         content = moment(value).format('YYYY-MM-DD HH:mm:ss');
@@ -176,34 +203,38 @@ export class IndexLifecycleSummary extends Component {
         content = value;
       }
       content = content || '-';
-      const cell = [
-        <EuiDescriptionListTitle key={fieldName}>
-          <strong>{headers[fieldName]}</strong>
-        </EuiDescriptionListTitle>,
-        <EuiDescriptionListDescription key={fieldName + '_desc'}>
-          {content}
-        </EuiDescriptionListDescription>,
-      ];
+      const cell = (
+        <>
+          <EuiDescriptionListTitle key={fieldName}>
+            <strong>{label}</strong>
+          </EuiDescriptionListTitle>
+          <EuiDescriptionListDescription key={fieldName + '_desc'}>
+            {content}
+          </EuiDescriptionListDescription>
+        </>
+      );
       if (arrayIndex % 2 === 0) {
         rows.left.push(cell);
       } else {
         rows.right.push(cell);
       }
     });
-    rows.right.push(this.renderPhaseExecutionPopoverButton(ilm));
+    if (ilm.phase_execution) {
+      rows.right.push(this.renderPhaseExecutionPopoverButton(ilm));
+    }
     return rows;
   }
 
   render() {
     const {
-      index: { ilm = {} },
+      index: { ilm },
     } = this.props;
     if (!ilm.managed) {
       return null;
     }
     const { left, right } = this.buildRows();
     return (
-      <Fragment>
+      <>
         <EuiTitle size="s">
           <h3>
             <FormattedMessage
@@ -213,7 +244,7 @@ export class IndexLifecycleSummary extends Component {
           </h3>
         </EuiTitle>
         {ilm.step_info && ilm.step_info.type ? (
-          <Fragment>
+          <>
             <EuiSpacer size="s" />
             <EuiCallOut
               color="danger"
@@ -229,10 +260,10 @@ export class IndexLifecycleSummary extends Component {
               <EuiSpacer size="s" />
               {this.renderStackPopoverButton(ilm)}
             </EuiCallOut>
-          </Fragment>
+          </>
         ) : null}
-        {ilm.step_info && ilm.step_info.message && !ilm.step_info.stack_trace ? (
-          <Fragment>
+        {ilm.step_info && ilm.step_info!.message && !ilm.step_info!.stack_trace ? (
+          <>
             <EuiSpacer size="s" />
             <EuiCallOut
               color="primary"
@@ -243,9 +274,9 @@ export class IndexLifecycleSummary extends Component {
                 />
               }
             >
-              {ilm.step_info.message}
+              {ilm.step_info!.message}
             </EuiCallOut>
-          </Fragment>
+          </>
         ) : null}
         <EuiSpacer size="m" />
         <EuiFlexGroup>
@@ -256,7 +287,7 @@ export class IndexLifecycleSummary extends Component {
             <EuiDescriptionList type="column">{right}</EuiDescriptionList>
           </EuiFlexItem>
         </EuiFlexGroup>
-      </Fragment>
+      </>
     );
   }
 }
diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx
similarity index 81%
rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js
rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx
index 8d01f4a4c200..bb5642cf3a47 100644
--- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js
+++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx
@@ -8,22 +8,27 @@ import React from 'react';
 import { get, every, some } from 'lodash';
 import { i18n } from '@kbn/i18n';
 import { EuiSearchBar } from '@elastic/eui';
+import { ApplicationStart } from 'kibana/public';
+
+import { IndexManagementPluginSetup } from '../../../index_management/public';
 
 import { retryLifecycleForIndex } from '../application/services/api';
 import { IndexLifecycleSummary } from './components/index_lifecycle_summary';
+
 import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal';
 import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle_confirm_modal';
+import { Index } from '../application/services/policies/types';
 
 const stepPath = 'ilm.step';
 
-export const retryLifecycleActionExtension = ({ indices }) => {
+export const retryLifecycleActionExtension = ({ indices }: { indices: Index[] }) => {
   const allHaveErrors = every(indices, (index) => {
     return index.ilm && index.ilm.failed_step;
   });
   if (!allHaveErrors) {
     return null;
   }
-  const indexNames = indices.map(({ name }) => name);
+  const indexNames = indices.map(({ name }: Index) => name);
   return {
     requestMethod: retryLifecycleForIndex,
     icon: 'play',
@@ -35,22 +40,28 @@ export const retryLifecycleActionExtension = ({ indices }) => {
       'xpack.indexLifecycleMgmt.retryIndexLifecycleAction.retriedLifecycleMessage',
       {
         defaultMessage: 'Called retry lifecycle step for: {indexNames}',
-        values: { indexNames: indexNames.map((indexName) => `"${indexName}"`).join(', ') },
+        values: { indexNames: indexNames.map((indexName: string) => `"${indexName}"`).join(', ') },
       }
     ),
   };
 };
 
-export const removeLifecyclePolicyActionExtension = ({ indices, reloadIndices }) => {
+export const removeLifecyclePolicyActionExtension = ({
+  indices,
+  reloadIndices,
+}: {
+  indices: Index[];
+  reloadIndices: () => void;
+}) => {
   const allHaveIlm = every(indices, (index) => {
     return index.ilm && index.ilm.managed;
   });
   if (!allHaveIlm) {
     return null;
   }
-  const indexNames = indices.map(({ name }) => name);
+  const indexNames = indices.map(({ name }: Index) => name);
   return {
-    renderConfirmModal: (closeModal) => {
+    renderConfirmModal: (closeModal: () => void) => {
       return (
         <RemoveLifecyclePolicyConfirmModal
           indexNames={indexNames}
@@ -67,7 +78,15 @@ export const removeLifecyclePolicyActionExtension = ({ indices, reloadIndices })
   };
 };
 
-export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices, getUrlForApp }) => {
+export const addLifecyclePolicyActionExtension = ({
+  indices,
+  reloadIndices,
+  getUrlForApp,
+}: {
+  indices: Index[];
+  reloadIndices: () => void;
+  getUrlForApp: ApplicationStart['getUrlForApp'];
+}) => {
   if (indices.length !== 1) {
     return null;
   }
@@ -79,7 +98,7 @@ export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices, getU
   }
   const indexName = index.name;
   return {
-    renderConfirmModal: (closeModal) => {
+    renderConfirmModal: (closeModal: () => void) => {
       return (
         <AddLifecyclePolicyConfirmModal
           indexName={indexName}
@@ -97,12 +116,12 @@ export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices, getU
   };
 };
 
-export const ilmBannerExtension = (indices) => {
+export const ilmBannerExtension = (indices: Index[]) => {
   const { Query } = EuiSearchBar;
   if (!indices.length) {
     return null;
   }
-  const indicesWithLifecycleErrors = indices.filter((index) => {
+  const indicesWithLifecycleErrors = indices.filter((index: Index) => {
     return get(index, stepPath) === 'ERROR';
   });
   const numIndicesWithLifecycleErrors = indicesWithLifecycleErrors.length;
@@ -124,11 +143,14 @@ export const ilmBannerExtension = (indices) => {
   };
 };
 
-export const ilmSummaryExtension = (index, getUrlForApp) => {
+export const ilmSummaryExtension = (
+  index: Index,
+  getUrlForApp: ApplicationStart['getUrlForApp']
+) => {
   return <IndexLifecycleSummary index={index} getUrlForApp={getUrlForApp} />;
 };
 
-export const ilmFilterExtension = (indices) => {
+export const ilmFilterExtension = (indices: Index[]) => {
   const hasIlm = some(indices, (index) => index.ilm && index.ilm.managed);
   if (!hasIlm) {
     return [];
@@ -200,7 +222,9 @@ export const ilmFilterExtension = (indices) => {
   }
 };
 
-export const addAllExtensions = (extensionsService) => {
+export const addAllExtensions = (
+  extensionsService: IndexManagementPluginSetup['extensionsService']
+) => {
   extensionsService.addAction(retryLifecycleActionExtension);
   extensionsService.addAction(removeLifecyclePolicyActionExtension);
   extensionsService.addAction(addLifecyclePolicyActionExtension);
diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts
index ecf5ba21fe60..354e4fe67cd1 100644
--- a/x-pack/plugins/index_management/common/types/indices.ts
+++ b/x-pack/plugins/index_management/common/types/indices.ts
@@ -41,3 +41,18 @@ export interface IndexSettings {
   analysis?: AnalysisModule;
   [key: string]: any;
 }
+
+export interface Index {
+  health: string;
+  status: string;
+  name: string;
+  uuid: string;
+  primary: string;
+  replica: string;
+  documents: any;
+  size: any;
+  isFrozen: boolean;
+  aliases: string | string[];
+  data_stream?: string;
+  [key: string]: any;
+}
diff --git a/x-pack/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts
index a2e9a41feb16..538dcaf25c47 100644
--- a/x-pack/plugins/index_management/public/index.ts
+++ b/x-pack/plugins/index_management/public/index.ts
@@ -14,3 +14,5 @@ export const plugin = () => {
 export { IndexManagementPluginSetup };
 
 export { getIndexListUri } from './application/services/routing';
+
+export { Index } from '../common';
diff --git a/x-pack/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts
index 4d9409e4a516..bf52d8a09c84 100644
--- a/x-pack/plugins/index_management/server/index.ts
+++ b/x-pack/plugins/index_management/server/index.ts
@@ -18,5 +18,5 @@ export const config = {
 /** @public */
 export { Dependencies } from './types';
 export { IndexManagementPluginSetup } from './plugin';
-export { Index } from './types';
+export { Index } from '../common';
 export { IndexManagementConfig } from './config';
diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts
index ae10629e069e..e9eaec3e2242 100644
--- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts
+++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts
@@ -5,7 +5,8 @@
  */
 import { CatIndicesParams } from 'elasticsearch';
 import { IndexDataEnricher } from '../services';
-import { Index, CallAsCurrentUser } from '../types';
+import { CallAsCurrentUser } from '../types';
+import { Index } from '../index';
 
 interface Hit {
   health: string;
@@ -44,7 +45,9 @@ async function fetchIndicesCall(
   // This call retrieves alias and settings (incl. hidden status) information about indices
   const indices: GetIndicesResponse = await callAsCurrentUser('transport.request', {
     method: 'GET',
-    path: `/${indexNamesString}`,
+    // transport.request doesn't do any URI encoding, unlike other JS client APIs. This enables
+    // working with Logstash indices with names like %{[@metadata][beat]}-%{[@metadata][version]}.
+    path: `/${encodeURIComponent(indexNamesString)}`,
     query: {
       expand_wildcards: 'hidden,all',
     },
diff --git a/x-pack/plugins/index_management/server/services/index_data_enricher.ts b/x-pack/plugins/index_management/server/services/index_data_enricher.ts
index 7a62ce9f7a3c..80bdf76820f2 100644
--- a/x-pack/plugins/index_management/server/services/index_data_enricher.ts
+++ b/x-pack/plugins/index_management/server/services/index_data_enricher.ts
@@ -4,7 +4,8 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { Index, CallAsCurrentUser } from '../types';
+import { CallAsCurrentUser } from '../types';
+import { Index } from '../index';
 
 export type Enricher = (indices: Index[], callAsCurrentUser: CallAsCurrentUser) => Promise<Index[]>;
 
diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts
index cd3eb5dfecd4..fce0414dee93 100644
--- a/x-pack/plugins/index_management/server/types.ts
+++ b/x-pack/plugins/index_management/server/types.ts
@@ -26,19 +26,4 @@ export interface RouteDependencies {
   };
 }
 
-export interface Index {
-  health: string;
-  status: string;
-  name: string;
-  uuid: string;
-  primary: string;
-  replica: string;
-  documents: any;
-  size: any;
-  isFrozen: boolean;
-  aliases: string | string[];
-  data_stream?: string;
-  [key: string]: any;
-}
-
 export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser'];
diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx
index 5077bccdc1ca..c1d4fc8b8d3c 100644
--- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx
@@ -85,7 +85,7 @@ export const LogEntryRow = memo(
     ]);
 
     const handleOpenViewLogInContext = useCallback(() => {
-      openViewLogInContext?.(logEntry); // eslint-disable-line no-unused-expressions
+      openViewLogInContext?.(logEntry);
       trackMetric({ metric: 'view_in_context__stream' });
     }, [openViewLogInContext, logEntry, trackMetric]);
 
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx
index 028dd0d3a1a7..740fc8b7bafc 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx
@@ -73,7 +73,6 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
 
   const showLoadDataErrorNotification = useCallback(
     (error: Error) => {
-      // eslint-disable-next-line no-unused-expressions
       services.notifications?.toasts.addError(error, {
         title: loadDataErrorTitle,
       });
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
index de72ac5c5a57..b33eaf7e77bc 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
@@ -103,7 +103,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
         }),
       };
 
-      // eslint-disable-next-line no-unused-expressions
       navigateToApp?.('logs', { path: `/stream?${stringify(params)}` });
     },
     [queryTimeRange, navigateToApp]
diff --git a/x-pack/plugins/ingest_manager/package.json b/x-pack/plugins/ingest_manager/package.json
index 8826ed57ab10..871972729b9f 100644
--- a/x-pack/plugins/ingest_manager/package.json
+++ b/x-pack/plugins/ingest_manager/package.json
@@ -5,6 +5,7 @@
   "private": true,
   "license": "Elastic-License",
   "dependencies": {
-    "abort-controller": "^3.0.0"
+    "abort-controller": "^3.0.0",
+    "ajv": "^6.12.4"
   }
 }
diff --git a/x-pack/plugins/ingest_manager/server/errors.test.ts b/x-pack/plugins/ingest_manager/server/errors.test.ts
new file mode 100644
index 000000000000..70e3a3b4150a
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/errors.test.ts
@@ -0,0 +1,191 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import Boom from 'boom';
+import { httpServerMock } from 'src/core/server/mocks';
+import { createAppContextStartContractMock } from './mocks';
+
+import {
+  IngestManagerError,
+  RegistryError,
+  PackageNotFoundError,
+  defaultIngestErrorHandler,
+} from './errors';
+import { appContextService } from './services';
+
+describe('defaultIngestErrorHandler', () => {
+  let mockContract: ReturnType<typeof createAppContextStartContractMock>;
+  beforeEach(async () => {
+    // prevents `Logger not set.` and other appContext errors
+    mockContract = createAppContextStartContractMock();
+    appContextService.start(mockContract);
+  });
+
+  afterEach(async () => {
+    jest.clearAllMocks();
+    appContextService.stop();
+  });
+
+  describe('IngestManagerError', () => {
+    it('502: RegistryError', async () => {
+      const error = new RegistryError('xyz');
+      const response = httpServerMock.createResponseFactory();
+
+      await defaultIngestErrorHandler({ error, response });
+
+      // response
+      expect(response.ok).toHaveBeenCalledTimes(0);
+      expect(response.customError).toHaveBeenCalledTimes(1);
+      expect(response.customError).toHaveBeenCalledWith({
+        statusCode: 502,
+        body: { message: error.message },
+      });
+
+      // logging
+      expect(mockContract.logger?.error).toHaveBeenCalledTimes(1);
+      expect(mockContract.logger?.error).toHaveBeenCalledWith(error.message);
+    });
+
+    it('404: PackageNotFoundError', async () => {
+      const error = new PackageNotFoundError('123');
+      const response = httpServerMock.createResponseFactory();
+
+      await defaultIngestErrorHandler({ error, response });
+
+      // response
+      expect(response.ok).toHaveBeenCalledTimes(0);
+      expect(response.customError).toHaveBeenCalledTimes(1);
+      expect(response.customError).toHaveBeenCalledWith({
+        statusCode: 404,
+        body: { message: error.message },
+      });
+
+      // logging
+      expect(mockContract.logger?.error).toHaveBeenCalledTimes(1);
+      expect(mockContract.logger?.error).toHaveBeenCalledWith(error.message);
+    });
+
+    it('400: IngestManagerError', async () => {
+      const error = new IngestManagerError('123');
+      const response = httpServerMock.createResponseFactory();
+
+      await defaultIngestErrorHandler({ error, response });
+
+      // response
+      expect(response.ok).toHaveBeenCalledTimes(0);
+      expect(response.customError).toHaveBeenCalledTimes(1);
+      expect(response.customError).toHaveBeenCalledWith({
+        statusCode: 400,
+        body: { message: error.message },
+      });
+
+      // logging
+      expect(mockContract.logger?.error).toHaveBeenCalledTimes(1);
+      expect(mockContract.logger?.error).toHaveBeenCalledWith(error.message);
+    });
+  });
+
+  describe('Boom', () => {
+    it('500: constructor - one arg', async () => {
+      const error = new Boom('bam');
+      const response = httpServerMock.createResponseFactory();
+
+      await defaultIngestErrorHandler({ error, response });
+
+      // response
+      expect(response.ok).toHaveBeenCalledTimes(0);
+      expect(response.customError).toHaveBeenCalledTimes(1);
+      expect(response.customError).toHaveBeenCalledWith({
+        statusCode: 500,
+        body: { message: 'An internal server error occurred' },
+      });
+
+      // logging
+      expect(mockContract.logger?.error).toHaveBeenCalledTimes(1);
+      expect(mockContract.logger?.error).toHaveBeenCalledWith('An internal server error occurred');
+    });
+
+    it('custom: constructor - 2 args', async () => {
+      const error = new Boom('Problem doing something', {
+        statusCode: 456,
+      });
+      const response = httpServerMock.createResponseFactory();
+
+      await defaultIngestErrorHandler({ error, response });
+
+      // response
+      expect(response.ok).toHaveBeenCalledTimes(0);
+      expect(response.customError).toHaveBeenCalledTimes(1);
+      expect(response.customError).toHaveBeenCalledWith({
+        statusCode: 456,
+        body: { message: error.message },
+      });
+
+      // logging
+      expect(mockContract.logger?.error).toHaveBeenCalledTimes(1);
+      expect(mockContract.logger?.error).toHaveBeenCalledWith('Problem doing something');
+    });
+
+    it('400: Boom.badRequest', async () => {
+      const error = Boom.badRequest('nope');
+      const response = httpServerMock.createResponseFactory();
+
+      await defaultIngestErrorHandler({ error, response });
+
+      // response
+      expect(response.ok).toHaveBeenCalledTimes(0);
+      expect(response.customError).toHaveBeenCalledTimes(1);
+      expect(response.customError).toHaveBeenCalledWith({
+        statusCode: 400,
+        body: { message: error.message },
+      });
+
+      // logging
+      expect(mockContract.logger?.error).toHaveBeenCalledTimes(1);
+      expect(mockContract.logger?.error).toHaveBeenCalledWith('nope');
+    });
+
+    it('404: Boom.notFound', async () => {
+      const error = Boom.notFound('sorry');
+      const response = httpServerMock.createResponseFactory();
+
+      await defaultIngestErrorHandler({ error, response });
+
+      // response
+      expect(response.ok).toHaveBeenCalledTimes(0);
+      expect(response.customError).toHaveBeenCalledTimes(1);
+      expect(response.customError).toHaveBeenCalledWith({
+        statusCode: 404,
+        body: { message: error.message },
+      });
+
+      // logging
+      expect(mockContract.logger?.error).toHaveBeenCalledTimes(1);
+      expect(mockContract.logger?.error).toHaveBeenCalledWith('sorry');
+    });
+  });
+
+  describe('all other errors', () => {
+    it('500', async () => {
+      const error = new Error('something');
+      const response = httpServerMock.createResponseFactory();
+
+      await defaultIngestErrorHandler({ error, response });
+
+      // response
+      expect(response.ok).toHaveBeenCalledTimes(0);
+      expect(response.customError).toHaveBeenCalledTimes(1);
+      expect(response.customError).toHaveBeenCalledWith({
+        statusCode: 500,
+        body: { message: error.message },
+      });
+
+      // logging
+      expect(mockContract.logger?.error).toHaveBeenCalledTimes(1);
+      expect(mockContract.logger?.error).toHaveBeenCalledWith(error);
+    });
+  });
+});
diff --git a/x-pack/plugins/ingest_manager/server/errors.ts b/x-pack/plugins/ingest_manager/server/errors.ts
index e6ef4a51284b..9829a4de23d7 100644
--- a/x-pack/plugins/ingest_manager/server/errors.ts
+++ b/x-pack/plugins/ingest_manager/server/errors.ts
@@ -5,6 +5,26 @@
  */
 
 /* eslint-disable max-classes-per-file */
+import Boom, { isBoom } from 'boom';
+import {
+  RequestHandlerContext,
+  KibanaRequest,
+  IKibanaResponse,
+  KibanaResponseFactory,
+} from 'src/core/server';
+import { appContextService } from './services';
+
+type IngestErrorHandler = (
+  params: IngestErrorHandlerParams
+) => IKibanaResponse | Promise<IKibanaResponse>;
+
+interface IngestErrorHandlerParams {
+  error: IngestManagerError | Boom | Error;
+  response: KibanaResponseFactory;
+  request?: KibanaRequest;
+  context?: RequestHandlerContext;
+}
+
 export class IngestManagerError extends Error {
   constructor(message?: string) {
     super(message);
@@ -12,7 +32,7 @@ export class IngestManagerError extends Error {
   }
 }
 
-export const getHTTPResponseCode = (error: IngestManagerError): number => {
+const getHTTPResponseCode = (error: IngestManagerError): number => {
   if (error instanceof RegistryError) {
     return 502; // Bad Gateway
   }
@@ -23,6 +43,40 @@ export const getHTTPResponseCode = (error: IngestManagerError): number => {
   return 400; // Bad Request
 };
 
+export const defaultIngestErrorHandler: IngestErrorHandler = async ({
+  error,
+  response,
+}: IngestErrorHandlerParams): Promise<IKibanaResponse> => {
+  const logger = appContextService.getLogger();
+
+  // our "expected" errors
+  if (error instanceof IngestManagerError) {
+    // only log the message
+    logger.error(error.message);
+    return response.customError({
+      statusCode: getHTTPResponseCode(error),
+      body: { message: error.message },
+    });
+  }
+
+  // handle any older Boom-based errors or the few places our app uses them
+  if (isBoom(error)) {
+    // only log the message
+    logger.error(error.output.payload.message);
+    return response.customError({
+      statusCode: error.output.statusCode,
+      body: { message: error.output.payload.message },
+    });
+  }
+
+  // not sure what type of error this is. log as much as possible
+  logger.error(error);
+  return response.customError({
+    statusCode: 500,
+    body: { message: error.message },
+  });
+};
+
 export class RegistryError extends IngestManagerError {}
 export class RegistryConnectionError extends RegistryError {}
 export class RegistryResponseError extends RegistryError {}
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts
index 564f5d03e945..b0439b30e897 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts
@@ -7,19 +7,13 @@
 // handlers that handle events from agents in response to actions received
 
 import { RequestHandler } from 'kibana/server';
-import { TypeOf } from '@kbn/config-schema';
-import { PostAgentAcksRequestSchema } from '../../types/rest_spec';
 import { AcksService } from '../../services/agents';
 import { AgentEvent } from '../../../common/types/models';
-import { PostAgentAcksResponse } from '../../../common/types/rest_spec';
+import { PostAgentAcksRequest, PostAgentAcksResponse } from '../../../common/types/rest_spec';
 
 export const postAgentAcksHandlerBuilder = function (
   ackService: AcksService
-): RequestHandler<
-  TypeOf<typeof PostAgentAcksRequestSchema.params>,
-  undefined,
-  TypeOf<typeof PostAgentAcksRequestSchema.body>
-> {
+): RequestHandler<PostAgentAcksRequest['params'], undefined, PostAgentAcksRequest['body']> {
   return async (context, request, response) => {
     try {
       const soClient = ackService.getSavedObjectsClientContract(request);
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
index 2bce8daa6637..605e4db230ce 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
@@ -15,6 +15,7 @@ import {
   PostAgentEnrollResponse,
   GetAgentStatusResponse,
   PutAgentReassignResponse,
+  PostAgentEnrollRequest,
 } from '../../../common/types';
 import {
   GetAgentsRequestSchema,
@@ -22,8 +23,7 @@ import {
   UpdateAgentRequestSchema,
   DeleteAgentRequestSchema,
   GetOneAgentEventsRequestSchema,
-  PostAgentCheckinRequestSchema,
-  PostAgentEnrollRequestSchema,
+  PostAgentCheckinRequest,
   GetAgentStatusRequestSchema,
   PutAgentReassignRequestSchema,
 } from '../../types';
@@ -159,9 +159,9 @@ export const updateAgentHandler: RequestHandler<
 };
 
 export const postAgentCheckinHandler: RequestHandler<
-  TypeOf<typeof PostAgentCheckinRequestSchema.params>,
+  PostAgentCheckinRequest['params'],
   undefined,
-  TypeOf<typeof PostAgentCheckinRequestSchema.body>
+  PostAgentCheckinRequest['body']
 > = async (context, request, response) => {
   try {
     const soClient = appContextService.getInternalUserSOClient(request);
@@ -218,7 +218,7 @@ export const postAgentCheckinHandler: RequestHandler<
 export const postAgentEnrollHandler: RequestHandler<
   undefined,
   undefined,
-  TypeOf<typeof PostAgentEnrollRequestSchema.body>
+  PostAgentEnrollRequest['body']
 > = async (context, request, response) => {
   try {
     const soClient = appContextService.getInternalUserSOClient(request);
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
index a84b0f8d0a35..a2e5c742ad6b 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
@@ -9,7 +9,8 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { IRouter } from 'src/core/server';
+import { IRouter, RouteValidationResultFactory } from 'src/core/server';
+import Ajv from 'ajv';
 import { PLUGIN_ID, AGENT_API_ROUTES, LIMITED_CONCURRENCY_ROUTE_TAG } from '../../constants';
 import {
   GetAgentsRequestSchema,
@@ -17,13 +18,15 @@ import {
   GetOneAgentEventsRequestSchema,
   UpdateAgentRequestSchema,
   DeleteAgentRequestSchema,
-  PostAgentCheckinRequestSchema,
-  PostAgentEnrollRequestSchema,
-  PostAgentAcksRequestSchema,
+  PostAgentCheckinRequestBodyJSONSchema,
+  PostAgentCheckinRequestParamsJSONSchema,
+  PostAgentAcksRequestParamsJSONSchema,
+  PostAgentAcksRequestBodyJSONSchema,
   PostAgentUnenrollRequestSchema,
   GetAgentStatusRequestSchema,
   PostNewAgentActionRequestSchema,
   PutAgentReassignRequestSchema,
+  PostAgentEnrollRequestBodyJSONSchema,
 } from '../../types';
 import {
   getAgentsHandler,
@@ -43,6 +46,29 @@ import { appContextService } from '../../services';
 import { postAgentsUnenrollHandler } from './unenroll_handler';
 import { IngestManagerConfigType } from '../..';
 
+const ajv = new Ajv({
+  coerceTypes: true,
+  useDefaults: true,
+  removeAdditional: true,
+  allErrors: false,
+  nullable: true,
+});
+
+function schemaErrorsText(errors: Ajv.ErrorObject[], dataVar: any) {
+  return errors.map((e) => `${dataVar + (e.dataPath || '')} ${e.message}`).join(', ');
+}
+
+function makeValidator(jsonSchema: any) {
+  const validator = ajv.compile(jsonSchema);
+  return function validateWithAJV(data: any, r: RouteValidationResultFactory) {
+    if (validator(data)) {
+      return r.ok(data);
+    }
+
+    return r.badRequest(schemaErrorsText(validator.errors || [], data));
+  };
+}
+
 export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) => {
   // Get one
   router.get(
@@ -86,7 +112,10 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType)
   router.post(
     {
       path: AGENT_API_ROUTES.CHECKIN_PATTERN,
-      validate: PostAgentCheckinRequestSchema,
+      validate: {
+        params: makeValidator(PostAgentCheckinRequestParamsJSONSchema),
+        body: makeValidator(PostAgentCheckinRequestBodyJSONSchema),
+      },
       options: {
         tags: [],
         ...(pollingRequestTimeout
@@ -105,7 +134,9 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType)
   router.post(
     {
       path: AGENT_API_ROUTES.ENROLL_PATTERN,
-      validate: PostAgentEnrollRequestSchema,
+      validate: {
+        body: makeValidator(PostAgentEnrollRequestBodyJSONSchema),
+      },
       options: { tags: [LIMITED_CONCURRENCY_ROUTE_TAG] },
     },
     postAgentEnrollHandler
@@ -115,7 +146,10 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType)
   router.post(
     {
       path: AGENT_API_ROUTES.ACKS_PATTERN,
-      validate: PostAgentAcksRequestSchema,
+      validate: {
+        params: makeValidator(PostAgentAcksRequestParamsJSONSchema),
+        body: makeValidator(PostAgentAcksRequestBodyJSONSchema),
+      },
       options: { tags: [LIMITED_CONCURRENCY_ROUTE_TAG] },
     },
     postAgentAcksHandlerBuilder({
diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
index 6400d6e215f9..6d7252ffec41 100644
--- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
@@ -32,7 +32,7 @@ import {
   getLimitedPackages,
   getInstallationObject,
 } from '../../services/epm/packages';
-import { IngestManagerError, getHTTPResponseCode } from '../../errors';
+import { IngestManagerError, defaultIngestErrorHandler } from '../../errors';
 import { splitPkgKey } from '../../services/epm/registry';
 
 export const getCategoriesHandler: RequestHandler<
@@ -45,11 +45,8 @@ export const getCategoriesHandler: RequestHandler<
       response: res,
     };
     return response.ok({ body });
-  } catch (e) {
-    return response.customError({
-      statusCode: 500,
-      body: { message: e.message },
-    });
+  } catch (error) {
+    return defaultIngestErrorHandler({ error, response });
   }
 };
 
@@ -69,11 +66,8 @@ export const getListHandler: RequestHandler<
     return response.ok({
       body,
     });
-  } catch (e) {
-    return response.customError({
-      statusCode: 500,
-      body: { message: e.message },
-    });
+  } catch (error) {
+    return defaultIngestErrorHandler({ error, response });
   }
 };
 
@@ -87,11 +81,8 @@ export const getLimitedListHandler: RequestHandler = async (context, request, re
     return response.ok({
       body,
     });
-  } catch (e) {
-    return response.customError({
-      statusCode: 500,
-      body: { message: e.message },
-    });
+  } catch (error) {
+    return defaultIngestErrorHandler({ error, response });
   }
 };
 
@@ -112,11 +103,8 @@ export const getFileHandler: RequestHandler<TypeOf<typeof GetFileRequestSchema.p
       customResponseObj.headers = { 'Content-Type': contentType };
     }
     return response.custom(customResponseObj);
-  } catch (e) {
-    return response.customError({
-      statusCode: 500,
-      body: { message: e.message },
-    });
+  } catch (error) {
+    return defaultIngestErrorHandler({ error, response });
   }
 };
 
@@ -135,11 +123,8 @@ export const getInfoHandler: RequestHandler<TypeOf<typeof GetInfoRequestSchema.p
       response: res,
     };
     return response.ok({ body });
-  } catch (e) {
-    return response.customError({
-      statusCode: 500,
-      body: { message: e.message },
-    });
+  } catch (error) {
+    return defaultIngestErrorHandler({ error, response });
   }
 };
 
@@ -165,14 +150,12 @@ export const installPackageHandler: RequestHandler<
     };
     return response.ok({ body });
   } catch (e) {
+    // could have also done `return defaultIngestErrorHandler({ error: e, response })` at each of the returns,
+    // but doing it this way will log the outer/install errors before any inner/rollback errors
+    const defaultResult = await defaultIngestErrorHandler({ error: e, response });
     if (e instanceof IngestManagerError) {
-      logger.error(e);
-      return response.customError({
-        statusCode: getHTTPResponseCode(e),
-        body: { message: e.message },
-      });
+      return defaultResult;
     }
-
     // if there is an unknown server error, uninstall any package assets
     try {
       const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
@@ -183,11 +166,7 @@ export const installPackageHandler: RequestHandler<
     } catch (error) {
       logger.error(`could not remove failed installation ${error}`);
     }
-    logger.error(e);
-    return response.customError({
-      statusCode: 500,
-      body: { message: e.message },
-    });
+    return defaultResult;
   }
 };
 
@@ -203,16 +182,7 @@ export const deletePackageHandler: RequestHandler<TypeOf<
       response: res,
     };
     return response.ok({ body });
-  } catch (e) {
-    if (e.isBoom) {
-      return response.customError({
-        statusCode: e.output.statusCode,
-        body: { message: e.output.payload.message },
-      });
-    }
-    return response.customError({
-      statusCode: 500,
-      body: { message: e.message },
-    });
+  } catch (error) {
+    return defaultIngestErrorHandler({ error, response });
   }
 };
diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts
index 1daa63800f4e..ee7dab6ef1a8 100644
--- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts
@@ -9,7 +9,7 @@ import { outputService, appContextService } from '../../services';
 import { GetFleetStatusResponse, PostIngestSetupResponse } from '../../../common';
 import { setupIngestManager, setupFleet } from '../../services/setup';
 import { PostFleetSetupRequestSchema } from '../../types';
-import { IngestManagerError, getHTTPResponseCode } from '../../errors';
+import { defaultIngestErrorHandler } from '../../errors';
 
 export const getFleetStatusHandler: RequestHandler = async (context, request, response) => {
   const soClient = context.core.savedObjects.client;
@@ -46,11 +46,8 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re
     return response.ok({
       body,
     });
-  } catch (e) {
-    return response.customError({
-      statusCode: 500,
-      body: { message: e.message },
-    });
+  } catch (error) {
+    return defaultIngestErrorHandler({ error, response });
   }
 };
 
@@ -70,44 +67,22 @@ export const createFleetSetupHandler: RequestHandler<
     return response.ok({
       body: { isInitialized: true },
     });
-  } catch (e) {
-    return response.customError({
-      statusCode: 500,
-      body: { message: e.message },
-    });
+  } catch (error) {
+    return defaultIngestErrorHandler({ error, response });
   }
 };
 
 export const ingestManagerSetupHandler: RequestHandler = async (context, request, response) => {
   const soClient = context.core.savedObjects.client;
   const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser;
-  const logger = appContextService.getLogger();
+
   try {
     const body: PostIngestSetupResponse = { isInitialized: true };
     await setupIngestManager(soClient, callCluster);
     return response.ok({
       body,
     });
-  } catch (e) {
-    if (e instanceof IngestManagerError) {
-      logger.error(e.message);
-      return response.customError({
-        statusCode: getHTTPResponseCode(e),
-        body: { message: e.message },
-      });
-    }
-    if (e.isBoom) {
-      logger.error(e.output.payload.message);
-      return response.customError({
-        statusCode: e.output.statusCode,
-        body: { message: e.output.payload.message },
-      });
-    }
-    logger.error(e.message);
-    logger.error(e.stack);
-    return response.customError({
-      statusCode: 500,
-      body: { message: e.message },
-    });
+  } catch (error) {
+    return defaultIngestErrorHandler({ error, response });
   }
 };
diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx
index aabe4bd3e359..e01568cfbb3c 100644
--- a/x-pack/plugins/ingest_manager/server/types/index.tsx
+++ b/x-pack/plugins/ingest_manager/server/types/index.tsx
@@ -63,6 +63,9 @@ export {
   IndexTemplateMappings,
   Settings,
   SettingsSOAttributes,
+  // Agent Request types
+  PostAgentEnrollRequest,
+  PostAgentCheckinRequest,
 } from '../../common';
 
 export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser'];
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
index 3302b0ab84ba..43ee0c89126e 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
+++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
@@ -5,12 +5,7 @@
  */
 
 import { schema } from '@kbn/config-schema';
-import {
-  AckEventSchema,
-  NewAgentEventSchema,
-  AgentTypeSchema,
-  NewAgentActionSchema,
-} from '../models';
+import { NewAgentActionSchema } from '../models';
 
 export const GetAgentsRequestSchema = {
   query: schema.object({
@@ -27,37 +22,134 @@ export const GetOneAgentRequestSchema = {
   }),
 };
 
-export const PostAgentCheckinRequestSchema = {
-  params: schema.object({
-    agentId: schema.string(),
-  }),
-  body: schema.object({
-    status: schema.maybe(
-      schema.oneOf([schema.literal('online'), schema.literal('error'), schema.literal('degraded')])
-    ),
-    local_metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())),
-    events: schema.maybe(schema.arrayOf(NewAgentEventSchema)),
-  }),
+export const PostAgentCheckinRequestParamsJSONSchema = {
+  type: 'object',
+  properties: {
+    agentId: { type: 'string' },
+  },
+  required: ['agentId'],
 };
 
-export const PostAgentEnrollRequestSchema = {
-  body: schema.object({
-    type: AgentTypeSchema,
-    shared_id: schema.maybe(schema.string()),
-    metadata: schema.object({
-      local: schema.recordOf(schema.string(), schema.any()),
-      user_provided: schema.recordOf(schema.string(), schema.any()),
-    }),
-  }),
+export const PostAgentCheckinRequestBodyJSONSchema = {
+  type: 'object',
+  properties: {
+    status: { type: 'string', enum: ['online', 'error', 'degraded'] },
+    local_metadata: {
+      additionalProperties: {
+        anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'object' }],
+      },
+    },
+    events: {
+      type: 'array',
+      items: {
+        type: 'object',
+        properties: {
+          type: { type: 'string', enum: ['STATE', 'ERROR', 'ACTION_RESULT', 'ACTION'] },
+          subtype: {
+            type: 'string',
+            enum: [
+              'RUNNING',
+              'STARTING',
+              'IN_PROGRESS',
+              'CONFIG',
+              'FAILED',
+              'STOPPING',
+              'STOPPED',
+              'DEGRADED',
+              'DATA_DUMP',
+              'ACKNOWLEDGED',
+              'UNKNOWN',
+            ],
+          },
+          timestamp: { type: 'string' },
+          message: { type: 'string' },
+          payload: { type: 'object', additionalProperties: true },
+          agent_id: { type: 'string' },
+          action_id: { type: 'string' },
+          policy_id: { type: 'string' },
+          stream_id: { type: 'string' },
+        },
+        required: ['type', 'subtype', 'timestamp', 'message', 'agent_id'],
+        additionalProperties: false,
+      },
+    },
+  },
+  additionalProperties: false,
 };
 
-export const PostAgentAcksRequestSchema = {
-  body: schema.object({
-    events: schema.arrayOf(AckEventSchema),
-  }),
-  params: schema.object({
-    agentId: schema.string(),
-  }),
+export const PostAgentEnrollRequestBodyJSONSchema = {
+  type: 'object',
+  properties: {
+    type: { type: 'string', enum: ['EPHEMERAL', 'PERMANENT', 'TEMPORARY'] },
+    shared_id: { type: 'string' },
+    metadata: {
+      type: 'object',
+      properties: {
+        local: {
+          type: 'object',
+          additionalProperties: true,
+        },
+        user_provided: {
+          type: 'object',
+          additionalProperties: true,
+        },
+      },
+      additionalProperties: false,
+      required: ['local', 'user_provided'],
+    },
+  },
+  additionalProperties: false,
+  required: ['type', 'metadata'],
+};
+
+export const PostAgentAcksRequestParamsJSONSchema = {
+  type: 'object',
+  properties: {
+    agentId: { type: 'string' },
+  },
+  required: ['agentId'],
+};
+
+export const PostAgentAcksRequestBodyJSONSchema = {
+  type: 'object',
+  properties: {
+    events: {
+      type: 'array',
+      item: {
+        type: 'object',
+        properties: {
+          type: { type: 'string', enum: ['STATE', 'ERROR', 'ACTION_RESULT', 'ACTION'] },
+          subtype: {
+            type: 'string',
+            enum: [
+              'RUNNING',
+              'STARTING',
+              'IN_PROGRESS',
+              'CONFIG',
+              'FAILED',
+              'STOPPING',
+              'STOPPED',
+              'DEGRADED',
+              'DATA_DUMP',
+              'ACKNOWLEDGED',
+              'UNKNOWN',
+            ],
+          },
+          timestamp: { type: 'string' },
+          message: { type: 'string' },
+          payload: { type: 'object', additionalProperties: true },
+          agent_id: { type: 'string' },
+          action_id: { type: 'string' },
+          policy_id: { type: 'string' },
+          stream_id: { type: 'string' },
+        },
+        required: ['type', 'subtype', 'timestamp', 'message', 'agent_id', 'action_id'],
+        additionalProperties: false,
+      },
+    },
+  },
+  additionalProperties: false,
+  required: ['events'],
 };
 
 export const PostNewAgentActionRequestSchema = {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx
index 7fb92e89c9f6..e00f9c002e5b 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx
@@ -14,6 +14,11 @@ interface Props {
   editorProps: { [key: string]: any };
 }
 
+const defaultEditorOptions = {
+  minimap: { enabled: false },
+  lineNumbers: 'off',
+};
+
 export const XJsonEditor: FunctionComponent<Props> = ({ field, editorProps }) => {
   const { value, setValue } = field;
   const { xJson, setXJson, convertToJson } = Monaco.useXJsonMode(value);
@@ -31,7 +36,7 @@ export const XJsonEditor: FunctionComponent<Props> = ({ field, editorProps }) =>
       editorProps={{
         value: xJson,
         languageId: XJsonLang.ID,
-        options: { minimap: { enabled: false } },
+        options: defaultEditorOptions,
         onChange,
         ...editorProps,
       }}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx
index 9adb3957ea9f..bda64c0a7561 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx
@@ -33,10 +33,22 @@ export const ProcessorSettingsFields: FunctionComponent<Props> = ({ processor })
             const formDescriptor = getProcessorDescriptor(type as any);
 
             if (formDescriptor?.FieldsComponent) {
+              const renderedFields = (
+                <formDescriptor.FieldsComponent
+                  key={type}
+                  initialFieldValues={processor?.options}
+                />
+              );
               return (
                 <>
-                  <formDescriptor.FieldsComponent key={type} />
-                  <EuiHorizontalRule />
+                  {renderedFields ? (
+                    <>
+                      {renderedFields}
+                      <EuiHorizontalRule />
+                    </>
+                  ) : (
+                    renderedFields
+                  )}
                   <CommonProcessorFields />
                 </>
               );
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx
index 23425297f342..52750529684b 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx
@@ -28,13 +28,13 @@ const fieldsConfig: FieldsConfig = {
       defaultMessage: 'Value',
     }),
     helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldHelpText', {
-      defaultMessage: 'The value to be appended by this processor.',
+      defaultMessage: 'Values to append.',
     }),
     validations: [
       {
         validator: emptyField(
           i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueRequiredError', {
-            defaultMessage: 'A value to set is required.',
+            defaultMessage: 'A value is required.',
           })
         ),
       },
@@ -47,7 +47,7 @@ export const Append: FunctionComponent = () => {
     <>
       <FieldNameField
         helpText={i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.fieldHelpText', {
-          defaultMessage: 'The field to be appended to.',
+          defaultMessage: 'Field to append values to.',
         })}
       />
 
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx
index a76e1a6f3ce9..6633f9e5de94 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx
@@ -17,7 +17,10 @@ export const Bytes: FunctionComponent = () => {
       <FieldNameField
         helpText={i18n.translate(
           'xpack.ingestPipelines.pipelineEditor.bytesForm.fieldNameHelpText',
-          { defaultMessage: 'The field to convert.' }
+          {
+            defaultMessage:
+              'Field to convert. If the field contains an array, each array value is converted.',
+          }
         )}
       />
 
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx
index 599d2fdbfd41..70df18acfd0a 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx
@@ -5,7 +5,9 @@
  */
 
 import React, { FunctionComponent } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
 import { i18n } from '@kbn/i18n';
+import { EuiCode } from '@elastic/eui';
 
 import {
   FIELD_TYPES,
@@ -34,12 +36,15 @@ const fieldsConfig: FieldsConfig = {
         defaultMessage: 'Error distance',
       }
     ),
-    helpText: i18n.translate(
-      'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceHelpText',
-      {
-        defaultMessage:
-          'The difference between the resulting inscribed distance from center to side and the circle’s radius (measured in meters for geo_shape, unit-less for shape).',
-      }
+    helpText: () => (
+      <FormattedMessage
+        id="xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceHelpText"
+        defaultMessage="Difference between the side of the inscribed shape to the encompassing circle. Determines the accuracy of the output polygon. Measured in meters for {geo_shape}, but uses no units for {shape}."
+        values={{
+          geo_shape: <EuiCode>{'geo_shape'}</EuiCode>,
+          shape: <EuiCode>{'shape'}</EuiCode>,
+        }}
+      />
     ),
     validations: [
       {
@@ -66,7 +71,7 @@ const fieldsConfig: FieldsConfig = {
     }),
     helpText: i18n.translate(
       'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeFieldHelpText',
-      { defaultMessage: 'Which field mapping type is to be used.' }
+      { defaultMessage: 'Field mapping type to use when processing the output polygon.' }
     ),
     validations: [
       {
@@ -86,7 +91,7 @@ export const Circle: FunctionComponent = () => {
       <FieldNameField
         helpText={i18n.translate(
           'xpack.ingestPipelines.pipelineEditor.circleForm.fieldNameHelpText',
-          { defaultMessage: 'The string-valued field to trim whitespace from.' }
+          { defaultMessage: 'Field to convert.' }
         )}
       />
 
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx
index 8089b8e7dfad..1777cac2a561 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx
@@ -16,12 +16,12 @@ import {
 } from '../../../../../../../shared_imports';
 
 import { TextEditor } from '../../field_components';
-import { to, from } from '../shared';
+import { to, from, EDITOR_PX_HEIGHT } from '../shared';
 
 const ignoreFailureConfig: FieldConfig = {
   defaultValue: false,
   deserializer: to.booleanOrUndef,
-  serializer: from.defaultBoolToUndef(false),
+  serializer: from.undefinedIfValue(false),
   label: i18n.translate(
     'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureFieldLabel',
     {
@@ -40,7 +40,7 @@ const ifConfig: FieldConfig = {
     defaultMessage: 'Condition (optional)',
   }),
   helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldHelpText', {
-    defaultMessage: 'Conditionally execute this processor.',
+    defaultMessage: 'Conditionally run this processor.',
   }),
   type: FIELD_TYPES.TEXT,
 };
@@ -50,7 +50,7 @@ const tagConfig: FieldConfig = {
     defaultMessage: 'Tag (optional)',
   }),
   helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldHelpText', {
-    defaultMessage: 'An identifier for this processor. Useful for debugging and metrics.',
+    defaultMessage: 'Identifier for the processor. Useful for debugging and metrics.',
   }),
   type: FIELD_TYPES.TEXT,
 };
@@ -64,8 +64,11 @@ export const CommonProcessorFields: FunctionComponent = () => {
         componentProps={{
           editorProps: {
             languageId: 'painless',
-            height: 75,
-            options: { minimap: { enabled: false } },
+            height: EDITOR_PX_HEIGHT.extraSmall,
+            options: {
+              lineNumbers: 'off',
+              minimap: { enabled: false },
+            },
           },
         }}
         path="fields.if"
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx
index 63ebb47dfc57..3d38f9238cdd 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx
@@ -22,7 +22,7 @@ export const fieldsConfig: FieldsConfig = {
     type: FIELD_TYPES.TOGGLE,
     defaultValue: false,
     deserializer: to.booleanOrUndef,
-    serializer: from.defaultBoolToUndef(false),
+    serializer: from.undefinedIfValue(false),
     label: i18n.translate(
       'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreMissingFieldLabel',
       {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx
index e4ad90f61af0..326492344288 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx
@@ -5,7 +5,7 @@
  */
 import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
 import { i18n } from '@kbn/i18n';
-import React, { FunctionComponent } from 'react';
+import React, { FunctionComponent, ReactNode } from 'react';
 import { flow } from 'fp-ts/lib/function';
 import { map } from 'fp-ts/lib/Array';
 
@@ -68,13 +68,18 @@ export const ProcessorTypeField: FunctionComponent<Props> = ({ initialType }) =>
     <UseField<string> config={typeConfig} defaultValue={initialType} path="type">
       {(typeField) => {
         let selectedOptions: ProcessorTypeAndLabel[];
+        let description: string | ReactNode = '';
+
         if (typeField.value?.length) {
           const type = typeField.value;
-          const descriptor = getProcessorDescriptor(type);
-          selectedOptions = descriptor
-            ? [{ label: descriptor.label, value: type }]
-            : // If there is no label for this processor type, just use the type as the label
-              [{ label: type, value: type }];
+          const processorDescriptor = getProcessorDescriptor(type);
+          if (processorDescriptor) {
+            description = processorDescriptor.description || '';
+            selectedOptions = [{ label: processorDescriptor.label, value: type }];
+          } else {
+            // If there is no label for this processor type, just use the type as the label
+            selectedOptions = [{ label: type, value: type }];
+          }
         } else {
           selectedOptions = [];
         }
@@ -102,9 +107,7 @@ export const ProcessorTypeField: FunctionComponent<Props> = ({ initialType }) =>
           <EuiFormRow
             label={typeField.label}
             labelAppend={typeField.labelAppend}
-            helpText={
-              typeof typeField.helpText === 'function' ? typeField.helpText() : typeField.helpText
-            }
+            helpText={typeof description === 'function' ? description() : description}
             error={error}
             isInvalid={isInvalid}
             fullWidth
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx
index 9bf44425a7c5..69ce01777b61 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx
@@ -21,8 +21,7 @@ const fieldsConfig: FieldsConfig = {
     helpText: i18n.translate(
       'xpack.ingestPipelines.pipelineEditor.commonFields.targetFieldHelpText',
       {
-        defaultMessage:
-          'The field to assign the joined value to. If empty, the field is updated in-place.',
+        defaultMessage: 'Output field. If empty, the input field is updated in place.',
       }
     ),
   },
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx
index 2bf642dd9b51..7284bd6c6275 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx
@@ -30,7 +30,7 @@ const fieldsConfig: FieldsConfig = {
       defaultMessage: 'Type',
     }),
     helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.typeFieldHelpText', {
-      defaultMessage: 'The type to convert the existing value to.',
+      defaultMessage: 'Field data type for the output.',
     }),
     validations: [
       {
@@ -50,7 +50,7 @@ export const Convert: FunctionComponent = () => {
       <FieldNameField
         helpText={i18n.translate(
           'xpack.ingestPipelines.pipelineEditor.convertForm.fieldNameHelpText',
-          { defaultMessage: 'The field whose value is to be converted.' }
+          { defaultMessage: 'Field to convert.' }
         )}
       />
 
@@ -115,14 +115,7 @@ export const Convert: FunctionComponent = () => {
         path="fields.type"
       />
 
-      <TargetField
-        helpText={i18n.translate(
-          'xpack.ingestPipelines.pipelineEditor.convertForm.targetFieldHelpText',
-          {
-            defaultMessage: 'The field to assign the converted value to.',
-          }
-        )}
-      />
+      <TargetField />
 
       <IgnoreMissingField />
     </>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx
index 835177dd861d..471efaa56dea 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx
@@ -36,7 +36,7 @@ const isStringLengthOne: ValidationFunc = ({ value }) => {
         message: i18n.translate(
           'xpack.ingestPipelines.pipelineEditor.convertForm.separatorLengthError',
           {
-            defaultMessage: 'A separator value must be 1 character.',
+            defaultMessage: 'Must be a single character.',
           }
         ),
       }
@@ -52,7 +52,7 @@ const fieldsConfig: FieldsConfig = {
       defaultMessage: 'Target fields',
     }),
     helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldsHelpText', {
-      defaultMessage: 'The array of fields to assign extracted values to.',
+      defaultMessage: 'Output fields. Extracted values are mapped to these fields.',
     }),
     validations: [
       {
@@ -83,7 +83,7 @@ const fieldsConfig: FieldsConfig = {
     helpText: (
       <FormattedMessage
         id="xpack.ingestPipelines.pipelineEditor.convertForm.separatorHelpText"
-        defaultMessage="Separator used in CSV, has to be single character string. Default value is {value}."
+        defaultMessage="Delimiter used in the CSV data. Defaults to {value}."
         values={{ value: <EuiCode inline>{','}</EuiCode> }}
       />
     ),
@@ -102,7 +102,7 @@ const fieldsConfig: FieldsConfig = {
     helpText: (
       <FormattedMessage
         id="xpack.ingestPipelines.pipelineEditor.convertForm.quoteHelpText"
-        defaultMessage="Quote used in CSV, has to be single character string. Default value is {value}."
+        defaultMessage="Escape character used in the CSV data. Defaults to {value}."
         values={{ value: <EuiCode inline>{'"'}</EuiCode> }}
       />
     ),
@@ -115,7 +115,7 @@ const fieldsConfig: FieldsConfig = {
       defaultMessage: 'Trim',
     }),
     helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.trimFieldHelpText', {
-      defaultMessage: 'Trim whitespaces in unquoted fields',
+      defaultMessage: 'Remove whitespaces in unquoted CSV data.',
     }),
   },
   empty_value: {
@@ -127,7 +127,7 @@ const fieldsConfig: FieldsConfig = {
       'xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldHelpText',
       {
         defaultMessage:
-          'Value used to fill empty fields, empty fields will be skipped if this is not provided.',
+          'Used to fill empty fields. If no value is provided, empty fields are skipped.',
       }
     ),
   },
@@ -138,7 +138,7 @@ export const CSV: FunctionComponent = () => {
     <>
       <FieldNameField
         helpText={i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.fieldNameHelpText', {
-          defaultMessage: 'The field to extract data from.',
+          defaultMessage: 'Field containing CSV data.',
         })}
       />
 
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx
index 82fdc81e0a84..c2aab62cf893 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx
@@ -17,6 +17,7 @@ import {
 const { emptyField, isJsonField } = fieldValidators;
 
 import { XJsonEditor } from '../field_components';
+import { EDITOR_PX_HEIGHT } from './shared';
 
 const customConfig: FieldConfig = {
   type: FIELD_TYPES.TEXT,
@@ -78,7 +79,7 @@ export const Custom: FunctionComponent<Props> = ({ defaultOptions }) => {
       componentProps={{
         editorProps: {
           'data-test-subj': 'processorOptionsEditor',
-          height: 300,
+          height: EDITOR_PX_HEIGHT.large,
           'aria-label': i18n.translate(
             'xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel',
             {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx
index 7e3f8e0d7cd7..8d6d88d2b066 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx
@@ -33,7 +33,7 @@ const fieldsConfig: FieldsConfig = {
     }),
     helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.formatsFieldHelpText', {
       defaultMessage:
-        'An array of the expected date formats. Can be a java time pattern or one of the following formats: ISO8601, UNIX, UNIX_MS, or TAI64N.',
+        'Expected date formats. Provided formats are applied sequentially. Accepts a Java time pattern, ISO8601, UNIX, UNIX_MS, or TAI64N formats.',
     }),
     validations: [
       {
@@ -59,7 +59,7 @@ const fieldsConfig: FieldsConfig = {
     helpText: (
       <FormattedMessage
         id="xpack.ingestPipelines.pipelineEditor.dateForm.timezoneHelpText"
-        defaultMessage="The timezone to use when parsing the date. Default value is {timezone}."
+        defaultMessage="Timezone for the date. Defaults to {timezone}."
         values={{ timezone: <EuiCode inline>{'UTC'}</EuiCode> }}
       />
     ),
@@ -73,7 +73,7 @@ const fieldsConfig: FieldsConfig = {
     helpText: (
       <FormattedMessage
         id="xpack.ingestPipelines.pipelineEditor.dateForm.localeHelpText"
-        defaultMessage="The locale to use when parsing the date, relevant when parsing month names or week days. Default value is {timezone}."
+        defaultMessage="Locale for the date. Useful when parsing month or day names. Defaults to {timezone}."
         values={{ timezone: <EuiCode inline>{'ENGLISH'}</EuiCode> }}
       />
     ),
@@ -89,7 +89,7 @@ export const DateProcessor: FunctionComponent = () => {
       <FieldNameField
         helpText={i18n.translate(
           'xpack.ingestPipelines.pipelineEditor.dateForm.fieldNameHelpText',
-          { defaultMessage: 'The field to get the date from.' }
+          { defaultMessage: 'Field to convert.' }
         )}
       />
 
@@ -99,7 +99,7 @@ export const DateProcessor: FunctionComponent = () => {
         helpText={
           <FormattedMessage
             id="xpack.ingestPipelines.pipelineEditor.dateForm.targetFieldHelpText"
-            defaultMessage="The field that will hold the parsed date. Default field is {defaultField}."
+            defaultMessage="Output field. If empty, the input field is updated in place. Defaults to {defaultField}."
             values={{
               defaultField: <EuiCode inline>{'@timestamp'}</EuiCode>,
             }}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx
index 8cbc064c1c90..73fa54429734 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx
@@ -36,7 +36,8 @@ const fieldsConfig: FieldsConfig = {
     helpText: i18n.translate(
       'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingFieldHelpText',
       {
-        defaultMessage: 'How to round the date when formatting the date into the index name.',
+        defaultMessage:
+          'Time period used to round the date when formatting the date into the index name.',
       }
     ),
     validations: [
@@ -64,7 +65,7 @@ const fieldsConfig: FieldsConfig = {
     ),
     helpText: i18n.translate(
       'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldHelpText',
-      { defaultMessage: 'A prefix of the index name to be prepended before the printed date.' }
+      { defaultMessage: 'Prefix to add before the printed date in the index name.' }
     ),
   },
   index_name_format: {
@@ -79,7 +80,7 @@ const fieldsConfig: FieldsConfig = {
     helpText: (
       <FormattedMessage
         id="xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNameFormatFieldHelpText"
-        defaultMessage="The format to be used when printing the parsed date into the index name. Default value is {value}."
+        defaultMessage="Date format used to print the parsed date into the index name. Defaults to {value}."
         values={{ value: <EuiCode inline>{'yyyy-MM-dd'}</EuiCode> }}
       />
     ),
@@ -99,7 +100,7 @@ const fieldsConfig: FieldsConfig = {
     helpText: (
       <FormattedMessage
         id="xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateFormatsHelpText"
-        defaultMessage="An array of the expected date formats for parsing dates / timestamps in the document being preprocessed. Can be a java time pattern or one of the following formats: ISO8601, UNIX, UNIX_MS, or TAI64N. Default value is {value}."
+        defaultMessage="Expected date formats. Provided formats are applied sequentially. Accepts a Java time pattern, ISO8601, UNIX, UNIX_MS, or TAI64N formats. Defaults to {value}."
         values={{ value: <EuiCode inline>{"yyyy-MM-dd'T'HH:mm:ss.SSSXX"}</EuiCode> }}
       />
     ),
@@ -116,7 +117,7 @@ const fieldsConfig: FieldsConfig = {
     helpText: (
       <FormattedMessage
         id="xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.timezoneHelpText"
-        defaultMessage="The timezone to use when parsing the date and when date math index supports resolves expressions into concrete index names. Default value is {timezone}."
+        defaultMessage="Timezone used when parsing the date and constructing the index name expression. Defaults to {timezone}."
         values={{ timezone: <EuiCode inline>{'UTC'}</EuiCode> }}
       />
     ),
@@ -133,7 +134,7 @@ const fieldsConfig: FieldsConfig = {
     helpText: (
       <FormattedMessage
         id="xpack.ingestPipelines.pipelineEditor.dateIndexForm.localeHelpText"
-        defaultMessage="The locale to use when parsing the date from the document being preprocessed, relevant when parsing month names or week days. Default value is {locale}."
+        defaultMessage="Locale to use when parsing the date. Useful when parsing month or day names. Defaults to {locale}."
         values={{ locale: <EuiCode inline>{'ENGLISH'}</EuiCode> }}
       />
     ),
@@ -149,7 +150,7 @@ export const DateIndexName: FunctionComponent = () => {
       <FieldNameField
         helpText={i18n.translate(
           'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.fieldNameHelpText',
-          { defaultMessage: 'The field to get the date or timestamp from.' }
+          { defaultMessage: 'Field containing the date or timestamp.' }
         )}
       />
 
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx
index 5f9f55ced1a2..51bc54c5b372 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx
@@ -5,7 +5,7 @@
  */
 
 import React, { FunctionComponent } from 'react';
-import { EuiCode } from '@elastic/eui';
+import { EuiCode, EuiLink } from '@elastic/eui';
 import { i18n } from '@kbn/i18n';
 import { FormattedMessage } from '@kbn/i18n/react';
 import { TextEditor } from '../field_components';
@@ -16,62 +16,88 @@ import {
   fieldValidators,
   UseField,
   Field,
+  useKibana,
 } from '../../../../../../shared_imports';
 
 import { FieldNameField } from './common_fields/field_name_field';
 import { IgnoreMissingField } from './common_fields/ignore_missing_field';
+import { EDITOR_PX_HEIGHT } from './shared';
 
 const { emptyField } = fieldValidators;
 
-const fieldsConfig: Record<string, FieldConfig> = {
-  /* Required field config */
-  pattern: {
-    type: FIELD_TYPES.TEXT,
-    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldLabel', {
-      defaultMessage: 'Pattern',
-    }),
-    helpText: i18n.translate(
-      'xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText',
-      {
-        defaultMessage: 'The pattern to apply to the field.',
-      }
-    ),
-    validations: [
-      {
-        validator: emptyField(
-          i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternRequiredError', {
-            defaultMessage: 'A pattern value is required.',
-          })
-        ),
-      },
-    ],
-  },
-  /* Optional field config */
-  append_separator: {
-    type: FIELD_TYPES.TEXT,
-    label: i18n.translate(
-      'xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorparaotrFieldLabel',
-      {
-        defaultMessage: 'Append separator (optional)',
-      }
-    ),
-    helpText: (
-      <FormattedMessage
-        id="xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorHelpText"
-        defaultMessage="The character(s) that separate the appended fields. Default value is {value} (an empty string)."
-        values={{ value: <EuiCode inline>{'""'}</EuiCode> }}
-      />
-    ),
-  },
+const getFieldsConfig = (esDocUrl: string): Record<string, FieldConfig> => {
+  return {
+    /* Required field config */
+    pattern: {
+      type: FIELD_TYPES.TEXT,
+      label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldLabel', {
+        defaultMessage: 'Pattern',
+      }),
+      helpText: (
+        <FormattedMessage
+          id="xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText"
+          defaultMessage="Pattern used to dissect the specified field. The pattern is defined by the parts of the string to discard. Use a {keyModifier} to alter the dissection behavior."
+          values={{
+            keyModifier: (
+              <EuiLink
+                target="_blank"
+                external
+                href={esDocUrl + '/dissect-processor.html#dissect-key-modifiers'}
+              >
+                {i18n.translate(
+                  'xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText.dissectProcessorLink',
+                  {
+                    defaultMessage: 'key modifier',
+                  }
+                )}
+              </EuiLink>
+            ),
+          }}
+        />
+      ),
+      validations: [
+        {
+          validator: emptyField(
+            i18n.translate(
+              'xpack.ingestPipelines.pipelineEditor.dissectForm.patternRequiredError',
+              {
+                defaultMessage: 'A pattern value is required.',
+              }
+            )
+          ),
+        },
+      ],
+    },
+    /* Optional field config */
+    append_separator: {
+      type: FIELD_TYPES.TEXT,
+      label: i18n.translate(
+        'xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorparaotrFieldLabel',
+        {
+          defaultMessage: 'Append separator (optional)',
+        }
+      ),
+      helpText: (
+        <FormattedMessage
+          id="xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorHelpText"
+          defaultMessage="If you specify a key modifier, this character separates the fields when appending results. Defaults to {value}."
+          values={{ value: <EuiCode inline>{'""'}</EuiCode> }}
+        />
+      ),
+    },
+  };
 };
 
 export const Dissect: FunctionComponent = () => {
+  const { services } = useKibana();
+  const fieldsConfig = getFieldsConfig(services.documentation.getEsDocsBasePath());
+
   return (
     <>
       <FieldNameField
         helpText={i18n.translate(
           'xpack.ingestPipelines.pipelineEditor.dissectForm.fieldNameHelpText',
-          { defaultMessage: 'The field to dissect.' }
+          { defaultMessage: 'Field to dissect.' }
         )}
       />
 
@@ -80,7 +106,7 @@ export const Dissect: FunctionComponent = () => {
         component={TextEditor}
         componentProps={{
           editorProps: {
-            height: 75,
+            height: EDITOR_PX_HEIGHT.extraSmall,
             options: { minimap: { enabled: false } },
           },
         }}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx
index 4e50c61ac930..4f2aa2915fde 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx
@@ -18,7 +18,8 @@ const fieldsConfig: Record<string, FieldConfig> = {
       defaultMessage: 'Path',
     }),
     helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathHelpText', {
-      defaultMessage: 'Only required if the field to expand is part another object field.',
+      defaultMessage:
+        'Output field. Only required if the field to expand is part another object field.',
     }),
   },
 };
@@ -29,7 +30,7 @@ export const DotExpander: FunctionComponent = () => {
       <FieldNameField
         helpText={i18n.translate(
           'xpack.ingestPipelines.pipelineEditor.dotExpanderForm.fieldNameHelpText',
-          { defaultMessage: 'The field to expand into an object field.' }
+          { defaultMessage: 'Field containing dot notation.' }
         )}
         additionalValidations={[
           {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx
index 5986374b338c..ba1c55b731cc 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx
@@ -81,7 +81,7 @@ const fieldsConfig: FieldsConfig = {
     type: FIELD_TYPES.TOGGLE,
     defaultValue: true,
     deserializer: to.booleanOrUndef,
-    serializer: from.defaultBoolToUndef,
+    serializer: from.undefinedIfValue,
     label: i18n.translate('xpack.ingestPipelines.pipelineEditor.enrichForm.overrideFieldLabel', {
       defaultMessage: 'Override',
     }),
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx
index ce606af08689..c32a85310d21 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx
@@ -12,7 +12,7 @@ import { FIELD_TYPES, fieldValidators, UseField } from '../../../../../../shared
 import { XJsonEditor } from '../field_components';
 
 import { FieldNameField } from './common_fields/field_name_field';
-import { FieldsConfig, to } from './shared';
+import { FieldsConfig, to, EDITOR_PX_HEIGHT } from './shared';
 
 const { emptyField, isJsonField } = fieldValidators;
 
@@ -31,15 +31,18 @@ const fieldsConfig: FieldsConfig = {
     validations: [
       {
         validator: emptyField(
-          i18n.translate('xpack.ingestPipelines.pipelineEditor.failForm.processorRequiredError', {
-            defaultMessage: 'A processor is required.',
-          })
+          i18n.translate(
+            'xpack.ingestPipelines.pipelineEditor.foreachForm.processorRequiredError',
+            {
+              defaultMessage: 'A processor is required.',
+            }
+          )
         ),
       },
       {
         validator: isJsonField(
           i18n.translate(
-            'xpack.ingestPipelines.pipelineEditor.failForm.processorInvalidJsonError',
+            'xpack.ingestPipelines.pipelineEditor.foreachForm.processorInvalidJsonError',
             {
               defaultMessage: 'Invalid JSON',
             }
@@ -64,9 +67,9 @@ export const Foreach: FunctionComponent = () => {
         component={XJsonEditor}
         componentProps={{
           editorProps: {
-            height: 200,
+            height: EDITOR_PX_HEIGHT.medium,
             'aria-label': i18n.translate(
-              'xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel',
+              'xpack.ingestPipelines.pipelineEditor.foreachForm.optionsFieldAriaLabel',
               {
                 defaultMessage: 'Configuration JSON editor',
               }
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx
index 9bb1d679938e..c0624c988061 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx
@@ -61,7 +61,7 @@ const fieldsConfig: FieldsConfig = {
     type: FIELD_TYPES.TOGGLE,
     defaultValue: true,
     deserializer: to.booleanOrUndef,
-    serializer: from.defaultBoolToUndef(true),
+    serializer: from.undefinedIfValue(true),
     label: i18n.translate('xpack.ingestPipelines.pipelineEditor.geoIPForm.firstOnlyFieldLabel', {
       defaultMessage: 'First only',
     }),
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx
index d021038fda94..c5c6adbe2a7a 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx
@@ -19,7 +19,7 @@ import { XJsonEditor } from '../field_components';
 
 import { FieldNameField } from './common_fields/field_name_field';
 import { IgnoreMissingField } from './common_fields/ignore_missing_field';
-import { FieldsConfig, to, from } from './shared';
+import { FieldsConfig, to, from, EDITOR_PX_HEIGHT } from './shared';
 
 const { emptyField, isJsonField } = fieldValidators;
 
@@ -80,7 +80,7 @@ const fieldsConfig: FieldsConfig = {
     type: FIELD_TYPES.TOGGLE,
     defaultValue: false,
     deserializer: to.booleanOrUndef,
-    serializer: from.defaultBoolToUndef(false),
+    serializer: from.undefinedIfValue(false),
     label: i18n.translate('xpack.ingestPipelines.pipelineEditor.grokForm.traceMatchFieldLabel', {
       defaultMessage: 'Trace match',
     }),
@@ -110,7 +110,7 @@ export const Grok: FunctionComponent = () => {
         config={fieldsConfig.pattern_definitions}
         componentProps={{
           editorProps: {
-            height: 200,
+            height: EDITOR_PX_HEIGHT.medium,
             'aria-label': i18n.translate(
               'xpack.ingestPipelines.pipelineEditor.grokForm.patternDefinitionsAriaLabel',
               {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx
index a0bda245d667..a42df6873d57 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx
@@ -13,7 +13,7 @@ import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../..
 
 import { TextEditor } from '../field_components';
 
-import { FieldsConfig } from './shared';
+import { EDITOR_PX_HEIGHT, FieldsConfig } from './shared';
 import { FieldNameField } from './common_fields/field_name_field';
 import { IgnoreMissingField } from './common_fields/ignore_missing_field';
 import { TargetField } from './common_fields/target_field';
@@ -78,7 +78,7 @@ export const Gsub: FunctionComponent = () => {
         component={TextEditor}
         componentProps={{
           editorProps: {
-            height: 75,
+            height: EDITOR_PX_HEIGHT.extraSmall,
             options: { minimap: { enabled: false } },
           },
         }}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts
index 4974361bf041..e83560b4a44c 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts
@@ -24,3 +24,15 @@ export { HtmlStrip } from './html_strip';
 export { Inference } from './inference';
 export { Join } from './join';
 export { Json } from './json';
+export { Kv } from './kv';
+export { Lowercase } from './lowercase';
+export { Pipeline } from './pipeline';
+export { Remove } from './remove';
+export { Rename } from './rename';
+export { Script } from './script';
+export { SetProcessor } from './set';
+export { SetSecurityUser } from './set_security_user';
+export { Split } from './split';
+export { Sort } from './sort';
+
+export { FormFieldsComponent } from './shared';
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx
index 68281fc11f34..85f995fa77ce 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx
@@ -21,7 +21,7 @@ import { XJsonEditor } from '../field_components';
 
 import { TargetField } from './common_fields/target_field';
 
-import { FieldsConfig, to, from } from './shared';
+import { FieldsConfig, to, from, EDITOR_PX_HEIGHT } from './shared';
 
 const { emptyField, isJsonField } = fieldValidators;
 
@@ -177,7 +177,7 @@ export const Inference: FunctionComponent = () => {
         component={XJsonEditor}
         componentProps={{
           editorProps: {
-            height: 200,
+            height: EDITOR_PX_HEIGHT.medium,
             options: { minimap: { enabled: false } },
           },
         }}
@@ -192,7 +192,7 @@ export const Inference: FunctionComponent = () => {
         component={XJsonEditor}
         componentProps={{
           editorProps: {
-            height: 200,
+            height: EDITOR_PX_HEIGHT.medium,
             options: { minimap: { enabled: false } },
           },
         }}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx
index c35a5b463f57..ab077d3337f6 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx
@@ -35,7 +35,7 @@ const fieldsConfig: FieldsConfig = {
       {
         validator: emptyField(
           i18n.translate('xpack.ingestPipelines.pipelineEditor.joinForm.separatorRequiredError', {
-            defaultMessage: 'A separator value is required.',
+            defaultMessage: 'A value is required.',
           })
         ),
       },
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx
index 5c4c53b65b6d..b68b39832508 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx
@@ -29,7 +29,7 @@ const fieldsConfig: FieldsConfig = {
       defaultMessage: 'Add to root',
     }),
     deserializer: to.booleanOrUndef,
-    serializer: from.defaultBoolToUndef(false),
+    serializer: from.undefinedIfValue(false),
     helpText: i18n.translate(
       'xpack.ingestPipelines.pipelineEditor.jsonForm.addToRootFieldHelpText',
       {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx
new file mode 100644
index 000000000000..f51bf19ad180
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx
@@ -0,0 +1,202 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiCode } from '@elastic/eui';
+
+import {
+  FIELD_TYPES,
+  fieldValidators,
+  UseField,
+  Field,
+  ComboBoxField,
+  ToggleField,
+} from '../../../../../../shared_imports';
+
+import { FieldsConfig, from, to } from './shared';
+import { FieldNameField } from './common_fields/field_name_field';
+import { TargetField } from './common_fields/target_field';
+import { IgnoreMissingField } from './common_fields/ignore_missing_field';
+
+const { emptyField } = fieldValidators;
+
+const fieldsConfig: FieldsConfig = {
+  /* Required fields config */
+  field_split: {
+    type: FIELD_TYPES.TEXT,
+    deserializer: String,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitFieldLabel', {
+      defaultMessage: 'Field split',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitHelpText', {
+      defaultMessage: 'Regex pattern for splitting key-value pairs.',
+    }),
+    validations: [
+      {
+        validator: emptyField(
+          i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitRequiredError', {
+            defaultMessage: 'A value is required.',
+          })
+        ),
+      },
+    ],
+  },
+  value_split: {
+    type: FIELD_TYPES.TEXT,
+    deserializer: String,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitFieldLabel', {
+      defaultMessage: 'Value split',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitHelpText', {
+      defaultMessage: 'Regex pattern for splitting the key from the value within a key-value pair.',
+    }),
+    validations: [
+      {
+        validator: emptyField(
+          i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitRequiredError', {
+            defaultMessage: 'A value is required.',
+          })
+        ),
+      },
+    ],
+  },
+
+  /* Optional fields config */
+  include_keys: {
+    type: FIELD_TYPES.COMBO_BOX,
+    deserializer: to.arrayOfStrings,
+    serializer: from.optionalArrayOfStrings,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysFieldLabel', {
+      defaultMessage: 'Include keys',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysHelpText', {
+      defaultMessage:
+        'List of keys to filter and insert into document. Defaults to including all keys.',
+    }),
+  },
+
+  exclude_keys: {
+    type: FIELD_TYPES.COMBO_BOX,
+    deserializer: to.arrayOfStrings,
+    serializer: from.optionalArrayOfStrings,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysFieldLabel', {
+      defaultMessage: 'Exclude keys',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysHelpText', {
+      defaultMessage: 'List of keys to exclude from document.',
+    }),
+  },
+
+  prefix: {
+    type: FIELD_TYPES.TEXT,
+    deserializer: String,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixFieldLabel', {
+      defaultMessage: 'Prefix',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixHelpText', {
+      defaultMessage: 'Prefix to be added to extracted keys.',
+    }),
+  },
+
+  trim_key: {
+    type: FIELD_TYPES.TEXT,
+    deserializer: String,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyFieldLabel', {
+      defaultMessage: 'Trim key',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyHelpText', {
+      defaultMessage: 'Characters to trim from extracted keys.',
+    }),
+  },
+
+  trim_value: {
+    type: FIELD_TYPES.TEXT,
+    deserializer: String,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimValueFieldLabel', {
+      defaultMessage: 'Trim value',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimValueHelpText', {
+      defaultMessage: 'Characters to trim from extracted values.',
+    }),
+  },
+
+  strip_brackets: {
+    type: FIELD_TYPES.TOGGLE,
+    defaultValue: false,
+    deserializer: to.booleanOrUndef,
+    serializer: from.undefinedIfValue(false),
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.stripBracketsFieldLabel', {
+      defaultMessage: 'Strip brackets',
+    }),
+    helpText: (
+      <FormattedMessage
+        id="xpack.ingestPipelines.pipelineEditor.kvForm.stripBracketsHelpText"
+        defaultMessage="If true, strip brackets {paren}, {angle}, {square} as well as quotes {singleQuote} and {doubleQuote} from extracted values."
+        values={{
+          paren: <EuiCode>{'()'}</EuiCode>,
+          angle: <EuiCode>&lt;&gt;</EuiCode>,
+          square: <EuiCode>{'[]'}</EuiCode>,
+          singleQuote: <EuiCode>{"'"}</EuiCode>,
+          doubleQuote: <EuiCode>{'"'}</EuiCode>,
+        }}
+      />
+    ),
+  },
+};
+
+export const Kv: FunctionComponent = () => {
+  return (
+    <>
+      <FieldNameField
+        helpText={i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldNameHelpText', {
+          defaultMessage: 'Field to be parsed.',
+        })}
+      />
+
+      <UseField component={Field} config={fieldsConfig.field_split} path="fields.field_split" />
+
+      <UseField component={Field} config={fieldsConfig.value_split} path="fields.value_split" />
+
+      <TargetField
+        helpText={i18n.translate(
+          'xpack.ingestPipelines.pipelineEditor.kvForm.targetFieldHelpText',
+          {
+            defaultMessage:
+              'Field to insert the extracted keys into. Defaults to the root of the document.',
+          }
+        )}
+      />
+
+      <UseField
+        component={ComboBoxField}
+        config={fieldsConfig.include_keys}
+        path="fields.include_keys"
+      />
+
+      <UseField
+        component={ComboBoxField}
+        config={fieldsConfig.exclude_keys}
+        path="fields.exclude_keys"
+      />
+
+      <IgnoreMissingField />
+
+      <UseField component={Field} config={fieldsConfig.prefix} path="fields.prefix" />
+
+      <UseField component={Field} config={fieldsConfig.trim_key} path="fields.trim_key" />
+
+      <UseField component={Field} config={fieldsConfig.trim_value} path="fields.trim_value" />
+
+      <UseField
+        component={ToggleField}
+        config={fieldsConfig.strip_brackets}
+        path="fields.strip_brackets"
+      />
+    </>
+  );
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx
new file mode 100644
index 000000000000..9db313a05007
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiCode } from '@elastic/eui';
+
+import { FieldNameField } from './common_fields/field_name_field';
+import { TargetField } from './common_fields/target_field';
+import { IgnoreMissingField } from './common_fields/ignore_missing_field';
+
+export const Lowercase: FunctionComponent = () => {
+  return (
+    <>
+      <FieldNameField
+        helpText={i18n.translate(
+          'xpack.ingestPipelines.pipelineEditor.lowerCaseForm.fieldNameHelpText',
+          { defaultMessage: 'Field to lowercase.' }
+        )}
+      />
+
+      <TargetField
+        helpText={
+          <FormattedMessage
+            id="xpack.ingestPipelines.pipelineEditor.lowerCaseForm.targetFieldHelpText"
+            defaultMessage="Field to assign the converted value to. Defaults to {field}."
+            values={{
+              field: <EuiCode>{'field'}</EuiCode>,
+            }}
+          />
+        }
+      />
+
+      <IgnoreMissingField />
+    </>
+  );
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx
new file mode 100644
index 000000000000..c785cf935833
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports';
+
+import { FieldsConfig } from './shared';
+
+const { emptyField } = fieldValidators;
+
+const fieldsConfig: FieldsConfig = {
+  /* Required fields config */
+  name: {
+    type: FIELD_TYPES.TEXT,
+    label: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldLabel',
+      {
+        defaultMessage: 'Pipeline name',
+      }
+    ),
+    deserializer: String,
+    helpText: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldHelpText',
+      {
+        defaultMessage: 'Name of the pipeline to execute.',
+      }
+    ),
+    validations: [
+      {
+        validator: emptyField(
+          i18n.translate(
+            'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameRequiredError',
+            {
+              defaultMessage: 'A value is required.',
+            }
+          )
+        ),
+      },
+    ],
+  },
+};
+
+export const Pipeline: FunctionComponent = () => {
+  return <UseField config={fieldsConfig.name} component={Field} path="fields.name" />;
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx
new file mode 100644
index 000000000000..3e90ce2b76f7
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import {
+  FIELD_TYPES,
+  UseField,
+  ComboBoxField,
+  fieldValidators,
+} from '../../../../../../shared_imports';
+
+import { FieldsConfig, to } from './shared';
+
+import { IgnoreMissingField } from './common_fields/ignore_missing_field';
+
+const { emptyField } = fieldValidators;
+
+const fieldsConfig: FieldsConfig = {
+  field: {
+    type: FIELD_TYPES.COMBO_BOX,
+    deserializer: to.arrayOfStrings,
+    serializer: (v: string[]) => (v.length === 1 ? v[0] : v),
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameField', {
+      defaultMessage: 'Fields',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameHelpText', {
+      defaultMessage: 'Fields to be removed.',
+    }),
+    validations: [
+      {
+        validator: emptyField(
+          i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameRequiredError', {
+            defaultMessage: 'A value is required.',
+          })
+        ),
+      },
+    ],
+  },
+};
+
+export const Remove: FunctionComponent = () => {
+  return (
+    <>
+      <UseField config={fieldsConfig.field} component={ComboBoxField} path="fields.field" />
+
+      <IgnoreMissingField />
+    </>
+  );
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx
new file mode 100644
index 000000000000..8b796d966458
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { fieldValidators } from '../../../../../../shared_imports';
+
+import { FieldNameField } from './common_fields/field_name_field';
+import { TargetField } from './common_fields/target_field';
+import { IgnoreMissingField } from './common_fields/ignore_missing_field';
+
+const { emptyField } = fieldValidators;
+
+export const Rename: FunctionComponent = () => {
+  return (
+    <>
+      <FieldNameField
+        helpText={i18n.translate(
+          'xpack.ingestPipelines.pipelineEditor.renameForm.fieldNameHelpText',
+          { defaultMessage: 'Field to be renamed.' }
+        )}
+      />
+
+      <TargetField
+        label={i18n.translate('xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldLabel', {
+          defaultMessage: 'Target field',
+        })}
+        helpText={i18n.translate(
+          'xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldHelpText',
+          { defaultMessage: 'Name of the new field.' }
+        )}
+        validations={[
+          {
+            validator: emptyField(
+              i18n.translate(
+                'xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldRequiredError',
+                { defaultMessage: 'A value is required.' }
+              )
+            ),
+          },
+        ]}
+      />
+
+      <IgnoreMissingField />
+    </>
+  );
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx
new file mode 100644
index 000000000000..ae0bbbb490ae
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx
@@ -0,0 +1,186 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiCode, EuiSwitch, EuiFormRow } from '@elastic/eui';
+
+import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports';
+
+import { XJsonEditor, TextEditor } from '../field_components';
+
+import { FieldsConfig, to, from, FormFieldsComponent, EDITOR_PX_HEIGHT } from './shared';
+
+const { isJsonField, emptyField } = fieldValidators;
+
+const fieldsConfig: FieldsConfig = {
+  /* Required fields config */
+
+  id: {
+    type: FIELD_TYPES.TEXT,
+    deserializer: String,
+    label: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldLabel',
+      {
+        defaultMessage: 'Stored script ID',
+      }
+    ),
+    helpText: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldHelpText',
+      {
+        defaultMessage: 'Stored script reference.',
+      }
+    ),
+    validations: [
+      {
+        validator: emptyField(
+          i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.idRequiredError', {
+            defaultMessage: 'A value is required.',
+          })
+        ),
+      },
+    ],
+  },
+
+  source: {
+    type: FIELD_TYPES.TEXT,
+    deserializer: String,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldLabel', {
+      defaultMessage: 'Source',
+    }),
+    helpText: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldHelpText',
+      {
+        defaultMessage: 'Script to be executed.',
+      }
+    ),
+    validations: [
+      {
+        validator: emptyField(
+          i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.sourceRequiredError', {
+            defaultMessage: 'A value is required.',
+          })
+        ),
+      },
+    ],
+  },
+
+  /* Optional fields config */
+  lang: {
+    type: FIELD_TYPES.TEXT,
+    deserializer: String,
+    serializer: from.undefinedIfValue('painless'),
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.langFieldLabel', {
+      defaultMessage: 'Language (optional)',
+    }),
+    helpText: (
+      <FormattedMessage
+        id="xpack.ingestPipelines.pipelineEditor.scriptForm.langFieldHelpText"
+        defaultMessage="Script language. Defaults to {lang}."
+        values={{
+          lang: <EuiCode>{'painless'}</EuiCode>,
+        }}
+      />
+    ),
+  },
+
+  params: {
+    type: FIELD_TYPES.TEXT,
+    deserializer: to.jsonString,
+    serializer: from.optionalJson,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldLabel', {
+      defaultMessage: 'Parameters',
+    }),
+    helpText: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldHelpText',
+      {
+        defaultMessage: 'Script parameters.',
+      }
+    ),
+    validations: [
+      {
+        validator: (value) => {
+          if (value.value) {
+            return isJsonField(
+              i18n.translate(
+                'xpack.ingestPipelines.pipelineEditor.scriptForm.processorInvalidJsonError',
+                {
+                  defaultMessage: 'Invalid JSON',
+                }
+              )
+            )(value);
+          }
+        },
+      },
+    ],
+  },
+};
+
+export const Script: FormFieldsComponent = ({ initialFieldValues }) => {
+  const [showId, setShowId] = useState(() => !!initialFieldValues?.id);
+  return (
+    <>
+      <EuiFormRow>
+        <EuiSwitch
+          label={i18n.translate(
+            'xpack.ingestPipelines.pipelineEditor.scriptForm.useScriptIdToggleLabel',
+            { defaultMessage: 'Use stored script' }
+          )}
+          checked={showId}
+          onChange={() => setShowId((v) => !v)}
+        />
+      </EuiFormRow>
+
+      {showId ? (
+        <UseField key="fields.id" path="fields.id" component={Field} config={fieldsConfig.id} />
+      ) : (
+        <>
+          <UseField component={Field} config={fieldsConfig.lang} path="fields.lang" />
+
+          <UseField
+            key="fields.source"
+            path="fields.source"
+            component={TextEditor}
+            componentProps={{
+              editorProps: {
+                height: EDITOR_PX_HEIGHT.medium,
+                'aria-label': i18n.translate(
+                  'xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldAriaLabel',
+                  {
+                    defaultMessage: 'Source script JSON editor',
+                  }
+                ),
+                options: {
+                  minimap: { enabled: false },
+                  lineNumbers: 'off',
+                },
+              },
+            }}
+            config={fieldsConfig.source}
+          />
+        </>
+      )}
+
+      <UseField
+        component={XJsonEditor}
+        componentProps={{
+          editorProps: {
+            height: EDITOR_PX_HEIGHT.medium,
+            'aria-label': i18n.translate(
+              'xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldAriaLabel',
+              {
+                defaultMessage: 'Parameters JSON editor',
+              }
+            ),
+          },
+        }}
+        config={fieldsConfig.params}
+        path="fields.params"
+      />
+    </>
+  );
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx
index 88cea620ae80..c282be35e507 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx
@@ -6,9 +6,10 @@
 
 import React, { FunctionComponent } from 'react';
 import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiCode } from '@elastic/eui';
 
 import {
-  FieldConfig,
   FIELD_TYPES,
   fieldValidators,
   ToggleField,
@@ -16,46 +17,68 @@ import {
   Field,
 } from '../../../../../../shared_imports';
 
-const { emptyField } = fieldValidators;
+import { FieldsConfig, to, from } from './shared';
 
-const fieldConfig: FieldConfig = {
-  type: FIELD_TYPES.TEXT,
-  label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel', {
-    defaultMessage: 'Field',
-  }),
-  validations: [
-    {
-      validator: emptyField(
-        i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError', {
-          defaultMessage: 'A field value is required.',
-        })
-      ),
-    },
-  ],
-};
+import { FieldNameField } from './common_fields/field_name_field';
 
-const valueConfig: FieldConfig = {
-  type: FIELD_TYPES.TEXT,
-  label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', {
-    defaultMessage: 'Value',
-  }),
-  validations: [
-    {
-      validator: emptyField(
-        i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', {
-          defaultMessage: 'A value to set is required.',
-        })
-      ),
-    },
-  ],
-};
+const { emptyField } = fieldValidators;
 
-const overrideConfig: FieldConfig = {
-  defaultValue: false,
-  label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', {
-    defaultMessage: 'Override',
-  }),
-  type: FIELD_TYPES.TOGGLE,
+const fieldsConfig: FieldsConfig = {
+  /* Required fields config */
+  value: {
+    type: FIELD_TYPES.TEXT,
+    deserializer: String,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', {
+      defaultMessage: 'Value',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText', {
+      defaultMessage: 'Value to be set for the field',
+    }),
+    validations: [
+      {
+        validator: emptyField(
+          i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', {
+            defaultMessage: 'A value is required',
+          })
+        ),
+      },
+    ],
+  },
+  /* Optional fields config */
+  override: {
+    type: FIELD_TYPES.TOGGLE,
+    defaultValue: true,
+    deserializer: to.booleanOrUndef,
+    serializer: from.undefinedIfValue(true),
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', {
+      defaultMessage: 'Override',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldHelpText', {
+      defaultMessage: 'If disabled, fields containing non-null values will not be updated.',
+    }),
+  },
+  ignore_empty_value: {
+    type: FIELD_TYPES.TOGGLE,
+    defaultValue: false,
+    deserializer: to.booleanOrUndef,
+    serializer: from.undefinedIfValue(false),
+    label: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.setForm.ignoreEmptyValueFieldLabel',
+      {
+        defaultMessage: 'Ignore empty value',
+      }
+    ),
+    helpText: (
+      <FormattedMessage
+        id="xpack.ingestPipelines.pipelineEditor.setForm.ignoreEmptyValueFieldHelpText"
+        defaultMessage="If enabled and {valueField} is a template snippet that evaluates to {nullValue} or an empty string, quietly exit without modifying the document."
+        values={{
+          valueField: <EuiCode>{'value'}</EuiCode>,
+          nullValue: <EuiCode>{'null'}</EuiCode>,
+        }}
+      />
+    ),
+  },
 };
 
 /**
@@ -64,11 +87,21 @@ const overrideConfig: FieldConfig = {
 export const SetProcessor: FunctionComponent = () => {
   return (
     <>
-      <UseField config={fieldConfig} component={Field} path="fields.field" />
+      <FieldNameField
+        helpText={i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldNameField', {
+          defaultMessage: 'Field to insert or update',
+        })}
+      />
+
+      <UseField config={fieldsConfig.value} component={Field} path="fields.value" />
 
-      <UseField config={valueConfig} component={Field} path="fields.value" />
+      <UseField config={fieldsConfig.override} component={ToggleField} path="fields.override" />
 
-      <UseField config={overrideConfig} component={ToggleField} path="fields.override" />
+      <UseField
+        config={fieldsConfig.ignore_empty_value}
+        component={ToggleField}
+        path="fields.ignore_empty_value"
+      />
     </>
   );
 };
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx
new file mode 100644
index 000000000000..78128b3d54c7
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiCode } from '@elastic/eui';
+
+import { FIELD_TYPES, UseField, ComboBoxField } from '../../../../../../shared_imports';
+
+import { FieldsConfig, to, from } from './shared';
+
+import { FieldNameField } from './common_fields/field_name_field';
+
+const userProperties: string[] = [
+  'username',
+  'roles',
+  'email',
+  'full_name',
+  'metadata',
+  'api_key',
+  'realm',
+  'authentication_type',
+];
+
+const comboBoxOptions = userProperties.map((prop) => ({ label: prop }));
+const helpTextValues = userProperties.join(', ');
+
+const fieldsConfig: FieldsConfig = {
+  /* Optional fields config */
+  properties: {
+    type: FIELD_TYPES.COMBO_BOX,
+    deserializer: to.arrayOfStrings,
+    serializer: from.optionalArrayOfStrings,
+    label: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.propertiesFieldLabel',
+      {
+        defaultMessage: 'Properties (optional)',
+      }
+    ),
+    helpText: (
+      <FormattedMessage
+        id="xpack.ingestPipelines.pipelineEditor.setForm.propertiesFieldHelpText"
+        defaultMessage="User properties to add. Defaults to {value}."
+        values={{
+          value: <EuiCode>[{helpTextValues}]</EuiCode>,
+        }}
+      />
+    ),
+  },
+};
+
+export const SetSecurityUser: FunctionComponent = () => {
+  return (
+    <>
+      <FieldNameField
+        helpText={i18n.translate(
+          'xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.fieldNameField',
+          {
+            defaultMessage: 'Field to store the user information',
+          }
+        )}
+      />
+
+      <UseField
+        config={fieldsConfig.properties}
+        component={ComboBoxField}
+        componentProps={{
+          euiFieldProps: {
+            options: comboBoxOptions,
+            noSuggestions: false,
+          },
+        }}
+        path="fields.properties"
+      />
+    </>
+  );
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts
index 84b308dd9cd7..e45469e23e8a 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts
@@ -3,7 +3,7 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-
+import { FunctionComponent } from 'react';
 import * as rt from 'io-ts';
 import { isRight } from 'fp-ts/lib/Either';
 
@@ -31,7 +31,8 @@ export function isArrayOfStrings(v: unknown): v is string[] {
  */
 export const to = {
   booleanOrUndef: (v: unknown): boolean | undefined => (typeof v === 'boolean' ? v : undefined),
-  arrayOfStrings: (v: unknown): string[] => (isArrayOfStrings(v) ? v : []),
+  arrayOfStrings: (v: unknown): string[] =>
+    isArrayOfStrings(v) ? v : typeof v === 'string' && v.length ? [v] : [],
   jsonString: (v: unknown) => (v ? JSON.stringify(v, null, 2) : '{}'),
 };
 
@@ -62,7 +63,17 @@ export const from = {
       }
     }
   },
-  defaultBoolToUndef: (defaultBool: boolean) => (v: boolean) => (v === defaultBool ? undefined : v),
+  optionalArrayOfStrings: (v: string[]) => (v.length ? v : undefined),
+  undefinedIfValue: (value: any) => (v: boolean) => (v === value ? undefined : v),
+};
+
+export const EDITOR_PX_HEIGHT = {
+  extraSmall: 75,
+  small: 100,
+  medium: 200,
+  large: 300,
 };
 
 export type FieldsConfig = Record<string, FieldConfig>;
+
+export type FormFieldsComponent = FunctionComponent<{ initialFieldValues?: Record<string, any> }>;
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx
new file mode 100644
index 000000000000..cdd0ff888acc
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { FIELD_TYPES, UseField, SelectField } from '../../../../../../shared_imports';
+
+import { FieldNameField } from './common_fields/field_name_field';
+import { TargetField } from './common_fields/target_field';
+import { FieldsConfig, from } from './shared';
+
+const fieldsConfig: FieldsConfig = {
+  /* Optional fields config */
+  order: {
+    type: FIELD_TYPES.SELECT,
+    defaultValue: 'asc',
+    deserializer: (v) => (v === 'asc' || v === 'desc' ? v : 'asc'),
+    serializer: from.undefinedIfValue('asc'),
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldLabel', {
+      defaultMessage: 'Order',
+    }),
+    helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldHelpText', {
+      defaultMessage: 'Sort order to use',
+    }),
+  },
+};
+
+export const Sort: FunctionComponent = () => {
+  return (
+    <>
+      <FieldNameField
+        helpText={i18n.translate(
+          'xpack.ingestPipelines.pipelineEditor.sortForm.fieldNameHelpText',
+          { defaultMessage: 'Field to sort' }
+        )}
+      />
+
+      <UseField
+        config={fieldsConfig.order}
+        component={SelectField}
+        componentProps={{
+          euiFieldProps: {
+            options: [
+              {
+                value: 'asc',
+                text: i18n.translate(
+                  'xpack.ingestPipelines.pipelineEditor.sortForm.orderField.ascendingOption',
+                  { defaultMessage: 'Ascending' }
+                ),
+              },
+              {
+                value: 'desc',
+                text: i18n.translate(
+                  'xpack.ingestPipelines.pipelineEditor.sortForm.orderField.descendingOption',
+                  { defaultMessage: 'Descending' }
+                ),
+              },
+            ],
+          },
+        }}
+        path="fields.order"
+      />
+
+      <TargetField />
+    </>
+  );
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx
new file mode 100644
index 000000000000..b48ce74110b3
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import {
+  FIELD_TYPES,
+  fieldValidators,
+  UseField,
+  Field,
+  ToggleField,
+} from '../../../../../../shared_imports';
+
+import { FieldNameField } from './common_fields/field_name_field';
+import { TargetField } from './common_fields/target_field';
+import { IgnoreMissingField } from './common_fields/ignore_missing_field';
+import { FieldsConfig, to, from } from './shared';
+
+const { emptyField } = fieldValidators;
+
+const fieldsConfig: FieldsConfig = {
+  /* Required fields config */
+  separator: {
+    type: FIELD_TYPES.TEXT,
+    label: i18n.translate('xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldLabel', {
+      defaultMessage: 'Separator',
+    }),
+    deserializer: String,
+    helpText: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldHelpText',
+      {
+        defaultMessage: 'Regex to match a separator',
+      }
+    ),
+    validations: [
+      {
+        validator: emptyField(
+          i18n.translate('xpack.ingestPipelines.pipelineEditor.splitForm.separatorRequiredError', {
+            defaultMessage: 'A value is required.',
+          })
+        ),
+      },
+    ],
+  },
+  /* Optional fields config */
+  preserve_trailing: {
+    type: FIELD_TYPES.TOGGLE,
+    defaultValue: false,
+    deserializer: to.booleanOrUndef,
+    serializer: from.undefinedIfValue(false),
+    label: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldLabel',
+      {
+        defaultMessage: 'Preserve trailing',
+      }
+    ),
+    helpText: i18n.translate(
+      'xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldHelpText',
+      { defaultMessage: 'If enabled, preserve any trailing space.' }
+    ),
+  },
+};
+
+export const Split: FunctionComponent = () => {
+  return (
+    <>
+      <FieldNameField
+        helpText={i18n.translate(
+          'xpack.ingestPipelines.pipelineEditor.splitForm.fieldNameHelpText',
+          { defaultMessage: 'Field to split' }
+        )}
+      />
+
+      <UseField config={fieldsConfig.separator} component={Field} path="fields.separator" />
+
+      <TargetField />
+
+      <UseField
+        config={fieldsConfig.preserve_trailing}
+        component={ToggleField}
+        path="fields.preserve_trailing"
+      />
+
+      <IgnoreMissingField />
+    </>
+  );
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx
index 854c6632ab94..799551b296ba 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx
@@ -5,7 +5,9 @@
  */
 
 import { i18n } from '@kbn/i18n';
-import { FunctionComponent } from 'react';
+import React, { ReactNode } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiCode } from '@elastic/eui';
 
 import {
   Append,
@@ -28,18 +30,27 @@ import {
   Inference,
   Join,
   Json,
+  Kv,
+  Lowercase,
+  Pipeline,
+  Remove,
+  Rename,
+  Script,
+  SetProcessor,
+  SetSecurityUser,
+  Split,
+  Sort,
+  FormFieldsComponent,
 } from '../manage_processor_form/processors';
 
-// import { SetProcessor } from './processors/set';
-// import { Gsub } from './processors/gsub';
-
 interface FieldDescriptor {
-  FieldsComponent?: FunctionComponent;
+  FieldsComponent?: FormFieldsComponent;
   docLinkPath: string;
   /**
    * A sentence case label that can be displayed to users
    */
   label: string;
+  description?: string | ReactNode;
 }
 
 type MapProcessorTypeToDescriptor = Record<string, FieldDescriptor>;
@@ -51,6 +62,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     label: i18n.translate('xpack.ingestPipelines.processors.label.append', {
       defaultMessage: 'Append',
     }),
+    description: i18n.translate('xpack.ingestPipelines.processors.description.append', {
+      defaultMessage:
+        "Appends values to a field's array. If the field contains a single value, the processor first converts it to an array. If the field doesn't exist, the processor creates an array containing the appended values.",
+    }),
   },
   bytes: {
     FieldsComponent: Bytes,
@@ -58,6 +73,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     label: i18n.translate('xpack.ingestPipelines.processors.label.bytes', {
       defaultMessage: 'Bytes',
     }),
+    description: i18n.translate('xpack.ingestPipelines.processors.description.bytes', {
+      defaultMessage:
+        'Converts digital storage units to bytes. For example, 1KB becomes 1024 bytes.',
+    }),
   },
   circle: {
     FieldsComponent: Circle,
@@ -65,6 +84,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     label: i18n.translate('xpack.ingestPipelines.processors.label.circle', {
       defaultMessage: 'Circle',
     }),
+    description: i18n.translate('xpack.ingestPipelines.processors.description.circle', {
+      defaultMessage: 'Converts a circle definition into an approximate polygon.',
+    }),
   },
   convert: {
     FieldsComponent: Convert,
@@ -72,6 +94,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     label: i18n.translate('xpack.ingestPipelines.processors.label.convert', {
       defaultMessage: 'Convert',
     }),
+    description: i18n.translate('xpack.ingestPipelines.processors.description.convert', {
+      defaultMessage:
+        'Converts a field to a different data type. For example, you can convert a string to an long.',
+    }),
   },
   csv: {
     FieldsComponent: CSV,
@@ -79,6 +105,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     label: i18n.translate('xpack.ingestPipelines.processors.label.csv', {
       defaultMessage: 'CSV',
     }),
+    description: i18n.translate('xpack.ingestPipelines.processors.description.csv', {
+      defaultMessage: 'Extracts fields values from CSV data.',
+    }),
   },
   date: {
     FieldsComponent: DateProcessor,
@@ -86,6 +115,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     label: i18n.translate('xpack.ingestPipelines.processors.label.date', {
       defaultMessage: 'Date',
     }),
+    description: i18n.translate('xpack.ingestPipelines.processors.description.date', {
+      defaultMessage: 'Converts a date to a document timestamp.',
+    }),
   },
   date_index_name: {
     FieldsComponent: DateIndexName,
@@ -93,6 +125,13 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     label: i18n.translate('xpack.ingestPipelines.processors.label.dateIndexName', {
       defaultMessage: 'Date index name',
     }),
+    description: () => (
+      <FormattedMessage
+        id="xpack.ingestPipelines.processors.description.dateIndexName"
+        defaultMessage="Uses a date or timestamp to add documents to the correct time-based index. Index names must use a date math pattern, such as {value}."
+        values={{ value: <EuiCode inline>{'my-index-yyyy-MM-dd'}</EuiCode> }}
+      />
+    ),
   },
   dissect: {
     FieldsComponent: Dissect,
@@ -100,6 +139,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     label: i18n.translate('xpack.ingestPipelines.processors.label.dissect', {
       defaultMessage: 'Dissect',
     }),
+    description: i18n.translate('xpack.ingestPipelines.processors.description.dissect', {
+      defaultMessage: 'Uses dissect patterns to extract matches from a field.',
+    }),
   },
   dot_expander: {
     FieldsComponent: DotExpander,
@@ -107,6 +149,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     label: i18n.translate('xpack.ingestPipelines.processors.label.dotExpander', {
       defaultMessage: 'Dot expander',
     }),
+    description: i18n.translate('xpack.ingestPipelines.processors.description.dotExpander', {
+      defaultMessage:
+        'Expands a field containing dot notation into an object field. The object field is then accessible by other processors in the pipeline.',
+    }),
   },
   drop: {
     FieldsComponent: Drop,
@@ -114,6 +160,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     label: i18n.translate('xpack.ingestPipelines.processors.label.drop', {
       defaultMessage: 'Drop',
     }),
+    description: i18n.translate('xpack.ingestPipelines.processors.description.drop', {
+      defaultMessage:
+        'Drops documents without returning an error. Used to only index documents that meet specified conditions.',
+    }),
   },
   enrich: {
     FieldsComponent: Enrich,
@@ -186,63 +236,70 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
     }),
   },
   kv: {
-    FieldsComponent: undefined, // TODO: Implement
+    FieldsComponent: Kv,
     docLinkPath: '/kv-processor.html',
     label: i18n.translate('xpack.ingestPipelines.processors.label.kv', {
       defaultMessage: 'KV',
     }),
   },
   lowercase: {
-    FieldsComponent: undefined, // TODO: Implement
+    FieldsComponent: Lowercase,
     docLinkPath: '/lowercase-processor.html',
     label: i18n.translate('xpack.ingestPipelines.processors.label.lowercase', {
       defaultMessage: 'Lowercase',
     }),
   },
   pipeline: {
-    FieldsComponent: undefined, // TODO: Implement
+    FieldsComponent: Pipeline,
     docLinkPath: '/pipeline-processor.html',
     label: i18n.translate('xpack.ingestPipelines.processors.label.pipeline', {
       defaultMessage: 'Pipeline',
     }),
   },
   remove: {
-    FieldsComponent: undefined, // TODO: Implement
+    FieldsComponent: Remove,
     docLinkPath: '/remove-processor.html',
     label: i18n.translate('xpack.ingestPipelines.processors.label.remove', {
       defaultMessage: 'Remove',
     }),
   },
   rename: {
-    FieldsComponent: undefined, // TODO: Implement
+    FieldsComponent: Rename,
     docLinkPath: '/rename-processor.html',
     label: i18n.translate('xpack.ingestPipelines.processors.label.rename', {
       defaultMessage: 'Rename',
     }),
   },
   script: {
-    FieldsComponent: undefined, // TODO: Implement
+    FieldsComponent: Script,
     docLinkPath: '/script-processor.html',
     label: i18n.translate('xpack.ingestPipelines.processors.label.script', {
       defaultMessage: 'Script',
     }),
   },
+  set: {
+    FieldsComponent: SetProcessor,
+    docLinkPath: '/set-processor.html',
+    label: i18n.translate('xpack.ingestPipelines.processors.label.set', {
+      defaultMessage: 'Set',
+    }),
+  },
   set_security_user: {
-    FieldsComponent: undefined, // TODO: Implement
+    FieldsComponent: SetSecurityUser,
     docLinkPath: '/ingest-node-set-security-user-processor.html',
     label: i18n.translate('xpack.ingestPipelines.processors.label.setSecurityUser', {
       defaultMessage: 'Set security user',
     }),
   },
   split: {
-    FieldsComponent: undefined, // TODO: Implement
+    FieldsComponent: Split,
     docLinkPath: '/split-processor.html',
     label: i18n.translate('xpack.ingestPipelines.processors.label.split', {
       defaultMessage: 'Split',
     }),
   },
   sort: {
-    FieldsComponent: undefined, // TODO: Implement
+    FieldsComponent: Sort,
     docLinkPath: '/sort-processor.html',
     label: i18n.translate('xpack.ingestPipelines.processors.label.sort', {
       defaultMessage: 'Sort',
@@ -276,15 +333,6 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
       defaultMessage: 'User agent',
     }),
   },
-
-  // --- The below processor descriptors have components implemented ---
-  set: {
-    FieldsComponent: undefined,
-    docLinkPath: '/set-processor.html',
-    label: i18n.translate('xpack.ingestPipelines.processors.label.set', {
-      defaultMessage: 'Set',
-    }),
-  },
 };
 
 export type ProcessorType = keyof typeof mapProcessorTypeToDescriptor;
diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts
index 936db37f0c62..abdbdf214040 100644
--- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts
+++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts
@@ -62,6 +62,7 @@ export {
   RadioGroupField,
   NumericField,
   SelectField,
+  CheckBoxField,
 } from '../../../../src/plugins/es_ui_shared/static/forms/components';
 
 export {
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
index 194f12cf9291..0db456e0760e 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
@@ -205,10 +205,10 @@ describe('Datatable Visualization', () => {
           },
           frame,
         }).groups
-      ).toHaveLength(1);
+      ).toHaveLength(2);
     });
 
-    it('allows all kinds of operations', () => {
+    it('allows only bucket operations one category', () => {
       const datasource = createMockDatasource('test');
       const frame = mockFrame();
       frame.datasourceLayers = { first: datasource.publicAPIMock };
@@ -232,6 +232,40 @@ describe('Datatable Visualization', () => {
       expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(true);
       expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(true);
       expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual(
+        false
+      );
+      expect(filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false })).toEqual(
+        false
+      );
+    });
+
+    it('allows only metric operations in one category', () => {
+      const datasource = createMockDatasource('test');
+      const frame = mockFrame();
+      frame.datasourceLayers = { first: datasource.publicAPIMock };
+
+      const filterOperations = datatableVisualization.getConfiguration({
+        layerId: 'first',
+        state: {
+          layers: [{ layerId: 'first', columns: [] }],
+        },
+        frame,
+      }).groups[1].filterOperations;
+
+      const baseOperation: Operation = {
+        dataType: 'string',
+        isBucketed: true,
+        label: '',
+      };
+      expect(filterOperations({ ...baseOperation })).toEqual(false);
+      expect(filterOperations({ ...baseOperation, dataType: 'number' })).toEqual(false);
+      expect(filterOperations({ ...baseOperation, dataType: 'date' })).toEqual(false);
+      expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(false);
+      expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(false);
+      expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual(
+        true
+      );
+      expect(filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false })).toEqual(
         true
       );
     });
@@ -248,7 +282,7 @@ describe('Datatable Visualization', () => {
           layerId: 'a',
           state: { layers: [layer] },
           frame,
-        }).groups[0].accessors
+        }).groups[1].accessors
       ).toEqual(['c', 'b']);
     });
   });
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
index 5aff4e14b17f..836ffcb15cfa 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
@@ -143,15 +143,29 @@ export const datatableVisualization: Visualization<DatatableVisualizationState>
       groups: [
         {
           groupId: 'columns',
-          groupLabel: i18n.translate('xpack.lens.datatable.columns', {
-            defaultMessage: 'Columns',
+          groupLabel: i18n.translate('xpack.lens.datatable.breakdown', {
+            defaultMessage: 'Break down by',
           }),
           layerId: state.layers[0].layerId,
-          accessors: sortedColumns,
+          accessors: sortedColumns.filter((c) => datasource.getOperationForColumnId(c)?.isBucketed),
           supportsMoreColumns: true,
-          filterOperations: () => true,
+          filterOperations: (op) => op.isBucketed,
           dataTestSubj: 'lnsDatatable_column',
         },
+        {
+          groupId: 'metrics',
+          groupLabel: i18n.translate('xpack.lens.datatable.metrics', {
+            defaultMessage: 'Metrics',
+          }),
+          layerId: state.layers[0].layerId,
+          accessors: sortedColumns.filter(
+            (c) => !datasource.getOperationForColumnId(c)?.isBucketed
+          ),
+          supportsMoreColumns: true,
+          filterOperations: (op) => !op.isBucketed,
+          required: true,
+          dataTestSubj: 'lnsDatatable_metrics',
+        },
       ],
     };
   },
diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
index d18a2db614f5..3581151dd5f7 100644
--- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
+++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
@@ -9,6 +9,20 @@ exports[`DragDrop droppable is reflected in the className 1`] = `
 </div>
 `;
 
+exports[`DragDrop items that have droppable=false get special styling when another item is dragged 1`] = `
+<div
+  className="lnsDragDrop lnsDragDrop-isNotDroppable"
+  data-test-subj="lnsDragDrop"
+  onDragEnd={[Function]}
+  onDragLeave={[Function]}
+  onDragOver={[Function]}
+  onDragStart={[Function]}
+  onDrop={[Function]}
+>
+  Hello!
+</div>
+`;
+
 exports[`DragDrop renders if nothing is being dragged 1`] = `
 <div
   class="lnsDragDrop"
diff --git a/x-pack/plugins/lens/public/drag_drop/_drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/_drag_drop.scss
index 5a4fb4e95ad0..c971540e165c 100644
--- a/x-pack/plugins/lens/public/drag_drop/_drag_drop.scss
+++ b/x-pack/plugins/lens/public/drag_drop/_drag_drop.scss
@@ -1,3 +1,7 @@
+.lnsDragDrop-isNotDroppable {
+  opacity: .5;
+}
+
 // Fix specificity by chaining classes
 
 .lnsDragDrop.lnsDragDrop-isDropTarget {
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx
index 765522067eaf..3240357c254e 100644
--- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx
@@ -49,7 +49,7 @@ describe('DragDrop', () => {
     const value = {};
 
     const component = mount(
-      <ChildDragDropProvider dragging={undefined} setDragging={setDragging}>
+      <ChildDragDropProvider dragging={value} setDragging={setDragging}>
         <DragDrop value={value} draggable={true} label="drag label">
           Hello!
         </DragDrop>
@@ -127,4 +127,63 @@ describe('DragDrop', () => {
 
     expect(component).toMatchSnapshot();
   });
+
+  test('items that have droppable=false get special styling when another item is dragged', () => {
+    const component = mount(
+      <ChildDragDropProvider dragging={'ignored'} setDragging={() => {}}>
+        <DragDrop value="ignored" draggable={true} label="a">
+          Ignored
+        </DragDrop>
+        <DragDrop onDrop={(x: unknown) => {}} droppable={false}>
+          Hello!
+        </DragDrop>
+      </ChildDragDropProvider>
+    );
+
+    expect(component.find('[data-test-subj="lnsDragDrop"]').at(1)).toMatchSnapshot();
+  });
+
+  test('additional styles are reflected in the className until drop', () => {
+    let dragging: string | undefined;
+    const getAdditionalClasses = jest.fn().mockReturnValue('additional');
+    const component = mount(
+      <ChildDragDropProvider
+        dragging={dragging}
+        setDragging={() => {
+          dragging = 'hello';
+        }}
+      >
+        <DragDrop value="ignored" draggable={true} label="a">
+          Ignored
+        </DragDrop>
+        <DragDrop
+          onDrop={(x: unknown) => {}}
+          droppable
+          getAdditionalClassesOnEnter={getAdditionalClasses}
+        >
+          Hello!
+        </DragDrop>
+      </ChildDragDropProvider>
+    );
+
+    const dataTransfer = {
+      setData: jest.fn(),
+      getData: jest.fn(),
+    };
+    component
+      .find('[data-test-subj="lnsDragDrop"]')
+      .first()
+      .simulate('dragstart', { dataTransfer });
+    jest.runAllTimers();
+
+    component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover');
+    expect(component.find('.additional')).toHaveLength(1);
+
+    component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave');
+    expect(component.find('.additional')).toHaveLength(0);
+
+    component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover');
+    component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop');
+    expect(component.find('.additional')).toHaveLength(0);
+  });
 });
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
index 5a0fc3b3839f..85bdd24bd4f8 100644
--- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
@@ -49,6 +49,11 @@ interface BaseProps {
    */
   droppable?: boolean;
 
+  /**
+   * Additional class names to apply when another element is over the drop target
+   */
+  getAdditionalClassesOnEnter?: () => string;
+
   /**
    * The optional test subject associated with this DOM element.
    */
@@ -97,6 +102,12 @@ export const DragDrop = (props: Props) => {
       {...props}
       dragging={droppable ? dragging : undefined}
       isDragging={!!(draggable && value === dragging)}
+      isNotDroppable={
+        // If the configuration has provided a droppable flag, but this particular item is not
+        // droppable, then it should be less prominent. Ignores items that are both
+        // draggable and drop targets
+        droppable === false && Boolean(dragging) && value !== dragging
+      }
       setDragging={setDragging}
     />
   );
@@ -107,9 +118,13 @@ const DragDropInner = React.memo(function DragDropInner(
     dragging: unknown;
     setDragging: (dragging: unknown) => void;
     isDragging: boolean;
+    isNotDroppable: boolean;
   }
 ) {
-  const [state, setState] = useState({ isActive: false });
+  const [state, setState] = useState({
+    isActive: false,
+    dragEnterClassNames: '',
+  });
   const {
     className,
     onDrop,
@@ -120,13 +135,20 @@ const DragDropInner = React.memo(function DragDropInner(
     dragging,
     setDragging,
     isDragging,
+    isNotDroppable,
   } = props;
 
-  const classes = classNames('lnsDragDrop', className, {
-    'lnsDragDrop-isDropTarget': droppable,
-    'lnsDragDrop-isActiveDropTarget': droppable && state.isActive,
-    'lnsDragDrop-isDragging': isDragging,
-  });
+  const classes = classNames(
+    'lnsDragDrop',
+    className,
+    {
+      'lnsDragDrop-isDropTarget': droppable,
+      'lnsDragDrop-isActiveDropTarget': droppable && state.isActive,
+      'lnsDragDrop-isDragging': isDragging,
+      'lnsDragDrop-isNotDroppable': isNotDroppable,
+    },
+    state.dragEnterClassNames
+  );
 
   const dragStart = (e: DroppableEvent) => {
     // Setting stopPropgagation causes Chrome failures, so
@@ -159,19 +181,25 @@ const DragDropInner = React.memo(function DragDropInner(
 
     // An optimization to prevent a bunch of React churn.
     if (!state.isActive) {
-      setState({ ...state, isActive: true });
+      setState({
+        ...state,
+        isActive: true,
+        dragEnterClassNames: props.getAdditionalClassesOnEnter
+          ? props.getAdditionalClassesOnEnter()
+          : '',
+      });
     }
   };
 
   const dragLeave = () => {
-    setState({ ...state, isActive: false });
+    setState({ ...state, isActive: false, dragEnterClassNames: '' });
   };
 
   const drop = (e: DroppableEvent) => {
     e.preventDefault();
     e.stopPropagation();
 
-    setState({ ...state, isActive: false });
+    setState({ ...state, isActive: false, dragEnterClassNames: '' });
     setDragging(undefined);
 
     if (onDrop && droppable) {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss
index 4e13fd95d196..62bc6d7ed7cc 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss
@@ -27,6 +27,14 @@
   overflow: hidden;
 }
 
+.lnsLayerPanel__dimension-isHidden {
+  opacity: 0;
+}
+
+.lnsLayerPanel__dimension-isReplacing {
+  text-decoration: line-through;
+}
+
 .lnsLayerPanel__triggerLink {
   padding: $euiSizeS;
   width: 100%;
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
index b3ad03b71770..85dbee6de524 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx
@@ -12,6 +12,7 @@ import {
   createMockDatasource,
   DatasourceMock,
 } from '../../mocks';
+import { ChildDragDropProvider } from '../../../drag_drop';
 import { EuiFormRow, EuiPopover } from '@elastic/eui';
 import { mount } from 'enzyme';
 import { mountWithIntl } from 'test_utils/enzyme_helpers';
@@ -272,6 +273,7 @@ describe('LayerPanel', () => {
 
       expect(component.find(EuiPopover).prop('isOpen')).toBe(true);
     });
+
     it('should close the popover when the active visualization changes', () => {
       /**
        * The ID generation system for new dimensions has been messy before, so
@@ -324,4 +326,151 @@ describe('LayerPanel', () => {
       expect(component.find(EuiPopover).prop('isOpen')).toBe(false);
     });
   });
+
+  // This test is more like an integration test, since the layer panel owns all
+  // the coordination between drag and drop
+  describe('drag and drop behavior', () => {
+    it('should determine if the datasource supports dropping of a field onto empty dimension', () => {
+      mockVisualization.getConfiguration.mockReturnValue({
+        groups: [
+          {
+            groupLabel: 'A',
+            groupId: 'a',
+            accessors: [],
+            filterOperations: () => true,
+            supportsMoreColumns: true,
+            dataTestSubj: 'lnsGroup',
+          },
+        ],
+      });
+
+      mockDatasource.canHandleDrop.mockReturnValue(true);
+
+      const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a' };
+
+      const component = mountWithIntl(
+        <ChildDragDropProvider dragging={draggingField} setDragging={jest.fn()}>
+          <LayerPanel {...getDefaultProps()} />
+        </ChildDragDropProvider>
+      );
+
+      expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith(
+        expect.objectContaining({
+          dragDropContext: expect.objectContaining({
+            dragging: draggingField,
+          }),
+        })
+      );
+
+      component.find('DragDrop[data-test-subj="lnsGroup"]').first().simulate('drop');
+
+      expect(mockDatasource.onDrop).toHaveBeenCalledWith(
+        expect.objectContaining({
+          dragDropContext: expect.objectContaining({
+            dragging: draggingField,
+          }),
+        })
+      );
+    });
+
+    it('should allow drag to move between groups', () => {
+      (generateId as jest.Mock).mockReturnValue(`newid`);
+
+      mockVisualization.getConfiguration.mockReturnValue({
+        groups: [
+          {
+            groupLabel: 'A',
+            groupId: 'a',
+            accessors: ['a'],
+            filterOperations: () => true,
+            supportsMoreColumns: false,
+            dataTestSubj: 'lnsGroupA',
+          },
+          {
+            groupLabel: 'B',
+            groupId: 'b',
+            accessors: ['b'],
+            filterOperations: () => true,
+            supportsMoreColumns: true,
+            dataTestSubj: 'lnsGroupB',
+          },
+        ],
+      });
+
+      mockDatasource.canHandleDrop.mockReturnValue(true);
+
+      const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a' };
+
+      const component = mountWithIntl(
+        <ChildDragDropProvider dragging={draggingOperation} setDragging={jest.fn()}>
+          <LayerPanel {...getDefaultProps()} />
+        </ChildDragDropProvider>
+      );
+
+      expect(mockDatasource.canHandleDrop).toHaveBeenCalledTimes(2);
+      expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith(
+        expect.objectContaining({
+          dragDropContext: expect.objectContaining({
+            dragging: draggingOperation,
+          }),
+        })
+      );
+
+      // Simulate drop on the pre-populated dimension
+      component.find('DragDrop[data-test-subj="lnsGroupB"]').at(0).simulate('drop');
+      expect(mockDatasource.onDrop).toHaveBeenCalledWith(
+        expect.objectContaining({
+          columnId: 'b',
+          dragDropContext: expect.objectContaining({
+            dragging: draggingOperation,
+          }),
+        })
+      );
+
+      // Simulate drop on the empty dimension
+      component.find('DragDrop[data-test-subj="lnsGroupB"]').at(1).simulate('drop');
+      expect(mockDatasource.onDrop).toHaveBeenCalledWith(
+        expect.objectContaining({
+          columnId: 'newid',
+          dragDropContext: expect.objectContaining({
+            dragging: draggingOperation,
+          }),
+        })
+      );
+    });
+
+    it('should prevent dropping in the same group', () => {
+      mockVisualization.getConfiguration.mockReturnValue({
+        groups: [
+          {
+            groupLabel: 'A',
+            groupId: 'a',
+            accessors: ['a', 'b'],
+            filterOperations: () => true,
+            supportsMoreColumns: true,
+            dataTestSubj: 'lnsGroup',
+          },
+        ],
+      });
+
+      const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a' };
+
+      const component = mountWithIntl(
+        <ChildDragDropProvider dragging={draggingOperation} setDragging={jest.fn()}>
+          <LayerPanel {...getDefaultProps()} />
+        </ChildDragDropProvider>
+      );
+
+      expect(mockDatasource.canHandleDrop).not.toHaveBeenCalled();
+
+      component.find('DragDrop[data-test-subj="lnsGroup"]').at(0).simulate('drop');
+      expect(mockDatasource.onDrop).not.toHaveBeenCalled();
+
+      component.find('DragDrop[data-test-subj="lnsGroup"]').at(1).simulate('drop');
+      expect(mockDatasource.onDrop).not.toHaveBeenCalled();
+
+      component.find('DragDrop[data-test-subj="lnsGroup"]').at(2).simulate('drop');
+      expect(mockDatasource.onDrop).not.toHaveBeenCalled();
+    });
+  });
 });
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
index b2804cfddba5..b45dd13bfa4f 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
@@ -17,8 +17,9 @@ import {
 } from '@elastic/eui';
 import { i18n } from '@kbn/i18n';
 import { FormattedMessage } from '@kbn/i18n/react';
+import classNames from 'classnames';
 import { NativeRenderer } from '../../../native_renderer';
-import { StateSetter } from '../../../types';
+import { StateSetter, isDraggedOperation } from '../../../types';
 import { DragContext, DragDrop, ChildDragDropProvider } from '../../../drag_drop';
 import { LayerSettings } from './layer_settings';
 import { trackUiEvent } from '../../../lens_ui_telemetry';
@@ -154,6 +155,7 @@ export function LayerPanel(
         {groups.map((group, index) => {
           const newId = generateId();
           const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
+
           return (
             <EuiFormRow
               className="lnsLayerPanel__row"
@@ -215,10 +217,32 @@ export function LayerPanel(
                   return (
                     <DragDrop
                       key={accessor}
-                      className="lnsLayerPanel__dimension"
+                      className={classNames('lnsLayerPanel__dimension', {
+                        // eslint-disable-next-line @typescript-eslint/naming-convention
+                        'lnsLayerPanel__dimension-isHidden':
+                          isDraggedOperation(dragDropContext.dragging) &&
+                          accessor === dragDropContext.dragging.columnId,
+                      })}
+                      getAdditionalClassesOnEnter={() => {
+                        // If we are dragging another column, add an indication that the behavior will be a replacement'
+                        if (
+                          isDraggedOperation(dragDropContext.dragging) &&
+                          group.groupId !== dragDropContext.dragging.groupId
+                        ) {
+                          return 'lnsLayerPanel__dimension-isReplacing';
+                        }
+                        return '';
+                      }}
                       data-test-subj={group.dataTestSubj}
+                      draggable={true}
+                      value={{ columnId: accessor, groupId: group.groupId, layerId }}
+                      label={group.groupLabel}
                       droppable={
-                        dragDropContext.dragging &&
+                        Boolean(dragDropContext.dragging) &&
+                        // Verify that the dragged item is not coming from the same group
+                        // since this would be a reorder
+                        (!isDraggedOperation(dragDropContext.dragging) ||
+                          dragDropContext.dragging.groupId !== group.groupId) &&
                         layerDatasource.canHandleDrop({
                           ...layerDatasourceDropProps,
                           columnId: accessor,
@@ -226,12 +250,22 @@ export function LayerPanel(
                         })
                       }
                       onDrop={(droppedItem) => {
-                        layerDatasource.onDrop({
+                        const dropResult = layerDatasource.onDrop({
                           ...layerDatasourceDropProps,
                           droppedItem,
                           columnId: accessor,
                           filterOperations: group.filterOperations,
                         });
+                        if (typeof dropResult === 'object') {
+                          // When a column is moved, we delete the reference to the old
+                          props.updateVisualization(
+                            activeVisualization.removeDimension({
+                              layerId,
+                              columnId: dropResult.deleted,
+                              prevState: props.visualizationState,
+                            })
+                          );
+                        }
                       }}
                     >
                       <DimensionPopover
@@ -315,7 +349,11 @@ export function LayerPanel(
                     className="lnsLayerPanel__dimension"
                     data-test-subj={group.dataTestSubj}
                     droppable={
-                      dragDropContext.dragging &&
+                      Boolean(dragDropContext.dragging) &&
+                      // Verify that the dragged item is not coming from the same group
+                      // since this would be a reorder
+                      (!isDraggedOperation(dragDropContext.dragging) ||
+                        dragDropContext.dragging.groupId !== group.groupId) &&
                       layerDatasource.canHandleDrop({
                         ...layerDatasourceDropProps,
                         columnId: newId,
@@ -323,13 +361,13 @@ export function LayerPanel(
                       })
                     }
                     onDrop={(droppedItem) => {
-                      const dropSuccess = layerDatasource.onDrop({
+                      const dropResult = layerDatasource.onDrop({
                         ...layerDatasourceDropProps,
                         droppedItem,
                         columnId: newId,
                         filterOperations: group.filterOperations,
                       });
-                      if (dropSuccess) {
+                      if (dropResult) {
                         props.updateVisualization(
                           activeVisualization.setDimension({
                             layerId,
@@ -338,6 +376,17 @@ export function LayerPanel(
                             prevState: props.visualizationState,
                           })
                         );
+
+                        if (typeof dropResult === 'object') {
+                          // When a column is moved, we delete the reference to the old
+                          props.updateVisualization(
+                            activeVisualization.removeDimension({
+                              layerId,
+                              columnId: dropResult.deleted,
+                              prevState: props.visualizationState,
+                            })
+                          );
+                        }
                       }
                     }}
                   >
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
index 3ee109376d97..f184d5628ab1 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
@@ -1378,6 +1378,66 @@ describe('IndexPatternDimensionEditorPanel', () => {
       ).toBe(false);
     });
 
+    it('is droppable if the dragged column is compatible', () => {
+      expect(
+        canHandleDrop({
+          ...defaultProps,
+          dragDropContext: {
+            ...dragDropContext,
+            dragging: {
+              columnId: 'col1',
+              groupId: 'a',
+              layerId: 'myLayer',
+            },
+          },
+          state: dragDropState(),
+          columnId: 'col2',
+          filterOperations: (op: OperationMetadata) => true,
+          layerId: 'myLayer',
+        })
+      ).toBe(true);
+    });
+
+    it('is not droppable if the dragged column is the same as the current column', () => {
+      expect(
+        canHandleDrop({
+          ...defaultProps,
+          dragDropContext: {
+            ...dragDropContext,
+            dragging: {
+              columnId: 'col1',
+              groupId: 'a',
+              layerId: 'myLayer',
+            },
+          },
+          state: dragDropState(),
+          columnId: 'col1',
+          filterOperations: (op: OperationMetadata) => true,
+          layerId: 'myLayer',
+        })
+      ).toBe(false);
+    });
+
+    it('is not droppable if the dragged column is incompatible', () => {
+      expect(
+        canHandleDrop({
+          ...defaultProps,
+          dragDropContext: {
+            ...dragDropContext,
+            dragging: {
+              columnId: 'col1',
+              groupId: 'a',
+              layerId: 'myLayer',
+            },
+          },
+          state: dragDropState(),
+          columnId: 'col2',
+          filterOperations: (op: OperationMetadata) => op.dataType === 'number',
+          layerId: 'myLayer',
+        })
+      ).toBe(false);
+    });
+
     it('appends the dropped column when a field is dropped', () => {
       const dragging = {
         field: { type: 'number', name: 'bar', aggregatable: true },
@@ -1526,5 +1586,109 @@ describe('IndexPatternDimensionEditorPanel', () => {
         },
       });
     });
+
+    it('updates the column id when moving an operation to an empty dimension', () => {
+      const dragging = {
+        columnId: 'col1',
+        groupId: 'a',
+        layerId: 'myLayer',
+      };
+      const testState = dragDropState();
+
+      onDrop({
+        ...defaultProps,
+        dragDropContext: {
+          ...dragDropContext,
+          dragging,
+        },
+        droppedItem: dragging,
+        state: testState,
+        columnId: 'col2',
+        filterOperations: (op: OperationMetadata) => true,
+        layerId: 'myLayer',
+      });
+
+      expect(setState).toBeCalledTimes(1);
+      expect(setState).toHaveBeenCalledWith({
+        ...testState,
+        layers: {
+          myLayer: {
+            ...testState.layers.myLayer,
+            columnOrder: ['col2'],
+            columns: {
+              col2: testState.layers.myLayer.columns.col1,
+            },
+          },
+        },
+      });
+    });
+
+    it('replaces an operation when moving to a populated dimension', () => {
+      const dragging = {
+        columnId: 'col2',
+        groupId: 'a',
+        layerId: 'myLayer',
+      };
+      const testState = dragDropState();
+      testState.layers.myLayer = {
+        indexPatternId: 'foo',
+        columnOrder: ['col1', 'col2', 'col3'],
+        columns: {
+          col1: testState.layers.myLayer.columns.col1,
+
+          col2: {
+            label: 'Top values of src',
+            dataType: 'string',
+            isBucketed: true,
+
+            // Private
+            operationType: 'terms',
+            params: {
+              orderBy: { type: 'column', columnId: 'col3' },
+              orderDirection: 'desc',
+              size: 10,
+            },
+            sourceField: 'src',
+          },
+          col3: {
+            label: 'Count',
+            dataType: 'number',
+            isBucketed: false,
+
+            // Private
+            operationType: 'count',
+            sourceField: 'Records',
+          },
+        },
+      };
+
+      onDrop({
+        ...defaultProps,
+        dragDropContext: {
+          ...dragDropContext,
+          dragging,
+        },
+        droppedItem: dragging,
+        state: testState,
+        columnId: 'col1',
+        filterOperations: (op: OperationMetadata) => true,
+        layerId: 'myLayer',
+      });
+
+      expect(setState).toBeCalledTimes(1);
+      expect(setState).toHaveBeenCalledWith({
+        ...testState,
+        layers: {
+          myLayer: {
+            ...testState.layers.myLayer,
+            columnOrder: ['col1', 'col3'],
+            columns: {
+              col1: testState.layers.myLayer.columns.col2,
+              col3: testState.layers.myLayer.columns.col3,
+            },
+          },
+        },
+      });
+    });
   });
 });
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx
index 1e8f73b19a3b..1fbbefd8f111 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx
@@ -15,6 +15,7 @@ import {
   DatasourceDimensionEditorProps,
   DatasourceDimensionDropProps,
   DatasourceDimensionDropHandlerProps,
+  isDraggedOperation,
 } from '../../types';
 import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
 import { IndexPatternColumn, OperationType } from '../indexpattern';
@@ -99,16 +100,25 @@ export function canHandleDrop(props: DatasourceDimensionDropProps<IndexPatternPr
     return Boolean(operationFieldSupportMatrix.operationByField[field.name]);
   }
 
-  return (
-    isDraggedField(dragging) &&
-    layerIndexPatternId === dragging.indexPatternId &&
-    Boolean(hasOperationForField(dragging.field))
-  );
+  if (isDraggedField(dragging)) {
+    return (
+      layerIndexPatternId === dragging.indexPatternId &&
+      Boolean(hasOperationForField(dragging.field))
+    );
+  }
+
+  if (
+    isDraggedOperation(dragging) &&
+    dragging.layerId === props.layerId &&
+    props.columnId !== dragging.columnId
+  ) {
+    const op = props.state.layers[props.layerId].columns[dragging.columnId];
+    return props.filterOperations(op);
+  }
+  return false;
 }
 
-export function onDrop(
-  props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>
-): boolean {
+export function onDrop(props: DatasourceDimensionDropHandlerProps<IndexPatternPrivateState>) {
   const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props);
   const droppedItem = props.droppedItem;
 
@@ -116,6 +126,42 @@ export function onDrop(
     return Boolean(operationFieldSupportMatrix.operationByField[field.name]);
   }
 
+  if (isDraggedOperation(droppedItem) && droppedItem.layerId === props.layerId) {
+    const layer = props.state.layers[props.layerId];
+    const op = { ...layer.columns[droppedItem.columnId] };
+    if (!props.filterOperations(op)) {
+      return false;
+    }
+
+    const newColumns = { ...layer.columns };
+    delete newColumns[droppedItem.columnId];
+    newColumns[props.columnId] = op;
+
+    const newColumnOrder = [...layer.columnOrder];
+    const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId);
+    const newIndex = newColumnOrder.findIndex((c) => c === props.columnId);
+
+    if (newIndex === -1) {
+      newColumnOrder[oldIndex] = props.columnId;
+    } else {
+      newColumnOrder.splice(oldIndex, 1);
+    }
+
+    // Time to replace
+    props.setState({
+      ...props.state,
+      layers: {
+        ...props.state.layers,
+        [props.layerId]: {
+          ...layer,
+          columnOrder: newColumnOrder,
+          columns: newColumns,
+        },
+      },
+    });
+    return { deleted: droppedItem.columnId };
+  }
+
   if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) {
     // TODO: What do we do if we couldn't find a column?
     return false;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts
index 0cd92fd96c95..374dbe77b4ca 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts
@@ -4,12 +4,12 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
+import { DataType } from '../types';
 import { DraggedField } from './indexpattern';
 import {
   BaseIndexPatternColumn,
   FieldBasedIndexPatternColumn,
 } from './operations/definitions/column_types';
-import { DataType } from '../types';
 
 /**
  * Normalizes the specified operation type. (e.g. document operations
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 729daed7223f..d8b77afdfe00 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -157,7 +157,7 @@ export interface Datasource<T = unknown, P = unknown> {
   renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps<T>) => void;
   renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps<T>) => void;
   canHandleDrop: (props: DatasourceDimensionDropProps<T>) => boolean;
-  onDrop: (props: DatasourceDimensionDropHandlerProps<T>) => boolean;
+  onDrop: (props: DatasourceDimensionDropHandlerProps<T>) => false | true | { deleted: string };
 
   toExpression: (state: T, layerId: string) => Ast | string | null;
 
@@ -230,6 +230,22 @@ export interface DatasourceLayerPanelProps<T> {
   setState: StateSetter<T>;
 }
 
+export interface DraggedOperation {
+  layerId: string;
+  groupId: string;
+  columnId: string;
+}
+
+export function isDraggedOperation(
+  operationCandidate: unknown
+): operationCandidate is DraggedOperation {
+  return (
+    typeof operationCandidate === 'object' &&
+    operationCandidate !== null &&
+    'columnId' in operationCandidate
+  );
+}
+
 export type DatasourceDimensionDropProps<T> = SharedDimensionProps & {
   layerId: string;
   columnId: string;
diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts
index b02a82f98af9..0c31015fc9f5 100644
--- a/x-pack/plugins/lists/public/exceptions/api.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/api.test.ts
@@ -375,7 +375,7 @@ describe('Exceptions Lists API', () => {
           namespace_type: 'single,single',
           page: '1',
           per_page: '20',
-          sort_field: 'created_at',
+          sort_field: 'exception-list.created_at',
           sort_order: 'desc',
         },
         signal: abortCtrl.signal,
@@ -408,7 +408,7 @@ describe('Exceptions Lists API', () => {
           namespace_type: 'single',
           page: '1',
           per_page: '20',
-          sort_field: 'created_at',
+          sort_field: 'exception-list.created_at',
           sort_order: 'desc',
         },
         signal: abortCtrl.signal,
@@ -441,7 +441,7 @@ describe('Exceptions Lists API', () => {
           namespace_type: 'agnostic',
           page: '1',
           per_page: '20',
-          sort_field: 'created_at',
+          sort_field: 'exception-list.created_at',
           sort_order: 'desc',
         },
         signal: abortCtrl.signal,
@@ -474,7 +474,7 @@ describe('Exceptions Lists API', () => {
           namespace_type: 'agnostic',
           page: '1',
           per_page: '20',
-          sort_field: 'created_at',
+          sort_field: 'exception-list.created_at',
           sort_order: 'desc',
         },
         signal: abortCtrl.signal,
@@ -508,7 +508,7 @@ describe('Exceptions Lists API', () => {
           namespace_type: 'agnostic',
           page: '1',
           per_page: '20',
-          sort_field: 'created_at',
+          sort_field: 'exception-list.created_at',
           sort_order: 'desc',
         },
         signal: abortCtrl.signal,
diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts
index 3f5ec8032050..824a25296260 100644
--- a/x-pack/plugins/lists/public/exceptions/api.ts
+++ b/x-pack/plugins/lists/public/exceptions/api.ts
@@ -288,7 +288,7 @@ export const fetchExceptionListsItemsByListIds = async ({
     namespace_type: namespaceTypes.join(','),
     page: pagination.page ? `${pagination.page}` : '1',
     per_page: pagination.perPage ? `${pagination.perPage}` : '20',
-    sort_field: 'created_at',
+    sort_field: 'exception-list.created_at',
     sort_order: 'desc',
     ...(filters.trim() !== '' ? { filter: filters } : {}),
   };
diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js
index 694e50cfe3e3..6cb1f87648da 100644
--- a/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js
+++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js
@@ -125,11 +125,11 @@ class PipelineEditorUi extends React.Component {
 
   onPipelineSave = () => {
     const { pipelineService, toastNotifications, intl } = this.props;
-    const { id } = this.state.pipeline;
+    const { id, ...pipelineToStore } = this.state.pipeline;
     return pipelineService
       .savePipeline({
         id,
-        upstreamJSON: this.state.pipeline,
+        upstreamJSON: pipelineToStore,
       })
       .then(() => {
         toastNotifications.addSuccess(
diff --git a/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts b/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts
index 8ce04c83afdb..0b7c3888b6f0 100755
--- a/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts
+++ b/x-pack/plugins/logstash/server/models/pipeline/pipeline.ts
@@ -11,14 +11,14 @@ import { i18n } from '@kbn/i18n';
 
 interface PipelineOptions {
   id: string;
-  description: string;
+  description?: string;
   pipeline: string;
   username?: string;
   settings?: Record<string, any>;
 }
 
 interface DownstreamPipeline {
-  description: string;
+  description?: string;
   pipeline: string;
   settings?: Record<string, any>;
 }
@@ -27,7 +27,7 @@ interface DownstreamPipeline {
  */
 export class Pipeline {
   public readonly id: string;
-  public readonly description: string;
+  public readonly description?: string;
   public readonly username?: string;
   public readonly pipeline: string;
   private readonly settings: Record<string, any>;
diff --git a/x-pack/plugins/logstash/server/routes/pipeline/save.ts b/x-pack/plugins/logstash/server/routes/pipeline/save.ts
index e484d0e221b6..755a82e670a2 100644
--- a/x-pack/plugins/logstash/server/routes/pipeline/save.ts
+++ b/x-pack/plugins/logstash/server/routes/pipeline/save.ts
@@ -22,8 +22,7 @@ export function registerPipelineSaveRoute(router: IRouter, security?: SecurityPl
           id: schema.string(),
         }),
         body: schema.object({
-          id: schema.string(),
-          description: schema.string(),
+          description: schema.maybe(schema.string()),
           pipeline: schema.string(),
           settings: schema.maybe(schema.object({}, { unknowns: 'allow' })),
         }),
diff --git a/x-pack/plugins/ml/common/constants/settings.ts b/x-pack/plugins/ml/common/constants/settings.ts
index 2df2ecd22e07..bab2aa2f2a0a 100644
--- a/x-pack/plugins/ml/common/constants/settings.ts
+++ b/x-pack/plugins/ml/common/constants/settings.ts
@@ -5,3 +5,11 @@
  */
 
 export const FILE_DATA_VISUALIZER_MAX_FILE_SIZE = 'ml:fileDataVisualizerMaxFileSize';
+export const ANOMALY_DETECTION_ENABLE_TIME_RANGE = 'ml:anomalyDetection:results:enableTimeDefaults';
+export const ANOMALY_DETECTION_DEFAULT_TIME_RANGE = 'ml:anomalyDetection:results:timeDefaults';
+
+export const DEFAULT_AD_RESULTS_TIME_FILTER = {
+  from: 'now-15m',
+  to: 'now',
+};
+export const DEFAULT_ENABLE_AD_RESULTS_TIME_FILTER = false;
diff --git a/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts
new file mode 100644
index 000000000000..368e758a027c
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { useCallback } from 'react';
+import { useMlKibana, useUiSettings } from '../../contexts/kibana';
+import {
+  ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
+  ANOMALY_DETECTION_ENABLE_TIME_RANGE,
+} from '../../../../common/constants/settings';
+import { mlJobService } from '../../services/job_service';
+
+export const useCreateADLinks = () => {
+  const {
+    services: {
+      http: { basePath },
+    },
+  } = useMlKibana();
+
+  const useUserTimeSettings = useUiSettings().get(ANOMALY_DETECTION_ENABLE_TIME_RANGE);
+  const userTimeSettings = useUiSettings().get(ANOMALY_DETECTION_DEFAULT_TIME_RANGE);
+  const createLinkWithUserDefaults = useCallback(
+    (location, jobList) => {
+      const resultsPageUrl = mlJobService.createResultsUrlForJobs(
+        jobList,
+        location,
+        useUserTimeSettings === true && userTimeSettings !== undefined
+          ? userTimeSettings
+          : undefined
+      );
+      return `${basePath.get()}/app/ml${resultsPageUrl}`;
+    },
+    [basePath]
+  );
+  return { createLinkWithUserDefaults };
+};
diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx
index 62a74ed142cc..6c57b3d08180 100644
--- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx
@@ -237,7 +237,7 @@ export const JobSelectorFlyout: FC<JobSelectorFlyoutProps> = ({
                 <EuiFlexItem grow={false}>
                   <EuiSwitch
                     label={i18n.translate('xpack.ml.jobSelector.applyTimerangeSwitchLabel', {
-                      defaultMessage: 'Apply timerange',
+                      defaultMessage: 'Apply time range',
                     })}
                     checked={applyTimeRange}
                     onChange={toggleTimerangeSwitch}
diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js
index 7b104ea372ae..1136487485f1 100644
--- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js
+++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js
@@ -174,7 +174,7 @@ export function JobSelectorTable({
         id: 'checkbox',
         isCheckbox: true,
         textOnly: false,
-        width: '24px',
+        width: '32px',
       },
       {
         label: 'group ID',
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js
index d78df80fad94..cea1159ebc14 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer.js
@@ -68,6 +68,7 @@ import { ExplorerChartsContainer } from './explorer_charts/explorer_charts_conta
 import { AnomaliesTable } from '../components/anomalies_table/anomalies_table';
 
 import { getTimefilter, getToastNotifications } from '../util/dependency_cache';
+import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings';
 
 const ExplorerPage = ({
   children,
@@ -145,6 +146,22 @@ export class Explorer extends React.Component {
   state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG };
   htmlIdGen = htmlIdGenerator();
 
+  componentDidMount() {
+    const { invalidTimeRangeError } = this.props;
+    if (invalidTimeRangeError) {
+      const toastNotifications = getToastNotifications();
+      toastNotifications.addWarning(
+        i18n.translate('xpack.ml.explorer.invalidTimeRangeInUrlCallout', {
+          defaultMessage:
+            'The time filter was changed to the full range due to an invalid default time filter. Check the advanced settings for {field}.',
+          values: {
+            field: ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
+          },
+        })
+      );
+    }
+  }
+
   // Escape regular parens from fieldName as that portion of the query is not wrapped in double quotes
   // and will cause a syntax error when called with getKqlQueryValues
   applyFilter = (fieldName, fieldValue, action) => {
@@ -298,7 +315,6 @@ export class Explorer extends React.Component {
 
           <div className={mainColumnClasses}>
             <EuiSpacer size="m" />
-
             {stoppedPartitions && (
               <EuiCallOut
                 size={'s'}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js
index 6b8d1d80aeda..d0d0442dd4ae 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js
@@ -8,16 +8,8 @@ import PropTypes from 'prop-types';
 import React from 'react';
 
 import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
-
-import { mlJobService } from '../../../../services/job_service';
 import { i18n } from '@kbn/i18n';
-import { getBasePath } from '../../../../util/dependency_cache';
-
-export function getLink(location, jobs) {
-  const basePath = getBasePath();
-  const resultsPageUrl = mlJobService.createResultsUrlForJobs(jobs, location);
-  return `${basePath.get()}/app/ml${resultsPageUrl}`;
-}
+import { useCreateADLinks } from '../../../../components/custom_hooks/use_create_ad_links';
 
 export function ResultLinks({ jobs }) {
   const openJobsInSingleMetricViewerText = i18n.translate(
@@ -44,13 +36,13 @@ export function ResultLinks({ jobs }) {
   const singleMetricVisible = jobs.length < 2;
   const singleMetricEnabled = jobs.length === 1 && jobs[0].isSingleMetricViewerJob;
   const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true;
-
+  const { createLinkWithUserDefaults } = useCreateADLinks();
   return (
     <React.Fragment>
       {singleMetricVisible && (
         <EuiToolTip position="bottom" content={openJobsInSingleMetricViewerText}>
           <EuiButtonIcon
-            href={getLink('timeseriesexplorer', jobs)}
+            href={createLinkWithUserDefaults('timeseriesexplorer', jobs)}
             iconType="visLine"
             aria-label={openJobsInSingleMetricViewerText}
             className="results-button"
@@ -61,7 +53,7 @@ export function ResultLinks({ jobs }) {
       )}
       <EuiToolTip position="bottom" content={openJobsInAnomalyExplorerText}>
         <EuiButtonIcon
-          href={getLink('explorer', jobs)}
+          href={createLinkWithUserDefaults('explorer', jobs)}
           iconType="visTable"
           aria-label={openJobsInAnomalyExplorerText}
           className="results-button"
diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx
index 07a555c13dbf..a71141d0356d 100644
--- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx
@@ -7,9 +7,8 @@
 import React, { FC } from 'react';
 import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui';
 import { i18n } from '@kbn/i18n';
-// @ts-ignore no module file
-import { getLink } from '../../../jobs/jobs_list/components/job_actions/results';
 import { MlSummaryJobs } from '../../../../../common/types/anomaly_detection_jobs';
+import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links';
 
 interface Props {
   jobsList: MlSummaryJobs;
@@ -23,13 +22,14 @@ export const ExplorerLink: FC<Props> = ({ jobsList }) => {
       values: { jobsCount: jobsList.length, jobId: jobsList[0] && jobsList[0].id },
     }
   );
+  const { createLinkWithUserDefaults } = useCreateADLinks();
 
   return (
     <EuiToolTip position="bottom" content={openJobsInAnomalyExplorerText}>
       <EuiButtonEmpty
         color="text"
         size="xs"
-        href={getLink('explorer', jobsList)}
+        href={createLinkWithUserDefaults('explorer', jobsList)}
         iconType="visTable"
         aria-label={openJobsInAnomalyExplorerText}
         className="results-button"
diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
index 2f2fc77283ef..f89e27925d74 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
@@ -73,7 +73,7 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
   const [globalState, setGlobalState] = useUrlState('_g');
   const [lastRefresh, setLastRefresh] = useState(0);
   const [stoppedPartitions, setStoppedPartitions] = useState<string[] | undefined>();
-
+  const [invalidTimeRangeError, setInValidTimeRangeError] = useState<boolean>(false);
   const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true });
 
   const { jobIds } = useJobSelection(jobsWithTimeRange);
@@ -99,6 +99,9 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
   // `timefilter.getBounds()` to update `bounds` in this component's state.
   useEffect(() => {
     if (globalState?.time !== undefined) {
+      if (globalState.time.mode === 'invalid') {
+        setInValidTimeRangeError(true);
+      }
       timefilter.setTime({
         from: globalState.time.from,
         to: globalState.time.to,
@@ -236,6 +239,7 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
           showCharts,
           severity: tableSeverity.val,
           stoppedPartitions,
+          invalidTimeRangeError,
         }}
       />
     </div>
diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
index 1f122ed18a85..817c97541599 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
@@ -91,6 +91,7 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
   const previousRefresh = usePrevious(lastRefresh);
   const [selectedJobId, setSelectedJobId] = useState<string>();
   const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true });
+  const [invalidTimeRangeError, setInValidTimeRangeError] = useState<boolean>(false);
 
   const refresh = useRefresh();
   useEffect(() => {
@@ -114,6 +115,9 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
   const [bounds, setBounds] = useState<TimeRangeBounds | undefined>(undefined);
   useEffect(() => {
     if (globalState?.time !== undefined) {
+      if (globalState.time.mode === 'invalid') {
+        setInValidTimeRangeError(true);
+      }
       timefilter.setTime({
         from: globalState.time.from,
         to: globalState.time.to,
@@ -300,6 +304,7 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
         tableSeverity: tableSeverity.val,
         timefilter,
         zoom: zoomProp,
+        invalidTimeRangeError,
       }}
     />
   );
diff --git a/x-pack/plugins/ml/public/application/services/job_service.d.ts b/x-pack/plugins/ml/public/application/services/job_service.d.ts
index 2134d157e1ba..30b2ec044285 100644
--- a/x-pack/plugins/ml/public/application/services/job_service.d.ts
+++ b/x-pack/plugins/ml/public/application/services/job_service.d.ts
@@ -5,6 +5,7 @@
  */
 
 import { SearchResponse } from 'elasticsearch';
+import { TimeRange } from 'src/plugins/data/common/query/timefilter/types';
 import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
 import { Calendar } from '../../../common/types/calendars';
 
@@ -15,7 +16,7 @@ export interface ExistingJobsAndGroups {
 
 declare interface JobService {
   jobs: CombinedJob[];
-  createResultsUrlForJobs: (jobs: any[], target: string) => string;
+  createResultsUrlForJobs: (jobs: any[], target: string, timeRange?: TimeRange) => string;
   tempJobCloningObjects: {
     job: any;
     skipTimeRangeStep: boolean;
diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js
index 704d76059f75..640f63617b7d 100644
--- a/x-pack/plugins/ml/public/application/services/job_service.js
+++ b/x-pack/plugins/ml/public/application/services/job_service.js
@@ -21,7 +21,7 @@ import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils';
 import { TIME_FORMAT } from '../../../common/constants/time_format';
 import { parseInterval } from '../../../common/util/parse_interval';
 import { toastNotificationServiceProvider } from '../services/toast_notification_service';
-
+import { validateTimeRange } from '../util/date_utils';
 const msgs = mlMessageBarService;
 let jobs = [];
 let datafeedIds = {};
@@ -790,8 +790,8 @@ class JobService {
     return groups;
   }
 
-  createResultsUrlForJobs(jobsList, resultsPage) {
-    return createResultsUrlForJobs(jobsList, resultsPage);
+  createResultsUrlForJobs(jobsList, resultsPage, timeRange) {
+    return createResultsUrlForJobs(jobsList, resultsPage, timeRange);
   }
 
   createResultsUrl(jobIds, from, to, resultsPage) {
@@ -932,31 +932,54 @@ function createJobStats(jobsList, jobStats) {
   jobStats.activeNodes.value = Object.keys(mlNodes).length;
 }
 
-function createResultsUrlForJobs(jobsList, resultsPage) {
+function createResultsUrlForJobs(jobsList, resultsPage, userTimeRange) {
   let from = undefined;
   let to = undefined;
-  if (jobsList.length === 1) {
-    from = jobsList[0].earliestTimestampMs;
-    to = jobsList[0].latestResultsTimestampMs; // Will be max(latest source data, latest bucket results)
+  let mode = 'absolute';
+  const jobIds = jobsList.map((j) => j.id);
+
+  // if the custom default time filter is set and enabled in advanced settings
+  // if time is either absolute date or proper datemath format
+  if (validateTimeRange(userTimeRange)) {
+    from = userTimeRange.from;
+    to = userTimeRange.to;
+    // if both pass datemath's checks but are not technically absolute dates, use 'quick'
+    // e.g. "now-15m" "now+1d"
+    const fromFieldAValidDate = moment(userTimeRange.from).isValid();
+    const toFieldAValidDate = moment(userTimeRange.to).isValid();
+    if (!fromFieldAValidDate && !toFieldAValidDate) {
+      return createResultsUrl(jobIds, from, to, resultsPage, 'quick');
+    }
   } else {
-    const jobsWithData = jobsList.filter((j) => j.earliestTimestampMs !== undefined);
-    if (jobsWithData.length > 0) {
-      from = Math.min(...jobsWithData.map((j) => j.earliestTimestampMs));
-      to = Math.max(...jobsWithData.map((j) => j.latestResultsTimestampMs));
+    // if time range is specified but with incorrect format
+    // change back to the default time range but alert the user
+    // that the advanced setting config is invalid
+    if (userTimeRange) {
+      mode = 'invalid';
+    }
+
+    if (jobsList.length === 1) {
+      from = jobsList[0].earliestTimestampMs;
+      to = jobsList[0].latestResultsTimestampMs; // Will be max(latest source data, latest bucket results)
+    } else {
+      const jobsWithData = jobsList.filter((j) => j.earliestTimestampMs !== undefined);
+      if (jobsWithData.length > 0) {
+        from = Math.min(...jobsWithData.map((j) => j.earliestTimestampMs));
+        to = Math.max(...jobsWithData.map((j) => j.latestResultsTimestampMs));
+      }
     }
   }
 
   const fromString = moment(from).format(TIME_FORMAT); // Defaults to 'now' if 'from' is undefined
   const toString = moment(to).format(TIME_FORMAT); // Defaults to 'now' if 'to' is undefined
 
-  const jobIds = jobsList.map((j) => j.id);
-  return createResultsUrl(jobIds, fromString, toString, resultsPage);
+  return createResultsUrl(jobIds, fromString, toString, resultsPage, mode);
 }
 
-function createResultsUrl(jobIds, start, end, resultsPage) {
+function createResultsUrl(jobIds, start, end, resultsPage, mode = 'absolute') {
   const idString = jobIds.map((j) => `'${j}'`).join(',');
-  const from = moment(start).toISOString();
-  const to = moment(end).toISOString();
+  let from;
+  let to;
   let path = '';
 
   if (resultsPage !== undefined) {
@@ -964,9 +987,20 @@ function createResultsUrl(jobIds, start, end, resultsPage) {
     path += resultsPage;
   }
 
+  if (mode === 'quick') {
+    from = start;
+    to = end;
+  } else {
+    from = moment(start).toISOString();
+    to = moment(end).toISOString();
+  }
+
   path += `?_g=(ml:(jobIds:!(${idString}))`;
   path += `,refreshInterval:(display:Off,pause:!f,value:0),time:(from:'${from}'`;
-  path += `,mode:absolute,to:'${to}'`;
+  path += `,to:'${to}'`;
+  if (mode === 'invalid') {
+    path += `,mode:invalid`;
+  }
   path += "))&_a=(query:(query_string:(analyze_wildcard:!t,query:'*')))";
 
   return path;
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
index 83a789074d35..0e99d64cf202 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
@@ -83,6 +83,7 @@ import {
   getFocusData,
 } from './timeseriesexplorer_utils';
 import { EMPTY_FIELD_VALUE_LABEL } from './components/entity_control/entity_control';
+import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings';
 
 // Used to indicate the chart is being plotted across
 // all partition field values, where the cardinality of the field cannot be
@@ -833,6 +834,22 @@ export class TimeSeriesExplorer extends React.Component {
   }
 
   componentDidMount() {
+    // if timeRange used in the url is incorrect
+    // perhaps due to user's advanced setting using incorrect date-maths
+    const { invalidTimeRangeError } = this.props;
+    if (invalidTimeRangeError) {
+      const toastNotifications = getToastNotifications();
+      toastNotifications.addWarning(
+        i18n.translate('xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout', {
+          defaultMessage:
+            'The time filter was changed to the full range for this job due to an invalid default time filter. Check the advanced settings for {field}.',
+          values: {
+            field: ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
+          },
+        })
+      );
+    }
+
     // Required to redraw the time series chart when the container is resized.
     this.resizeChecker = new ResizeChecker(this.resizeRef.current);
     this.resizeChecker.on('resize', () => {
diff --git a/x-pack/plugins/ml/public/application/util/date_utils.ts b/x-pack/plugins/ml/public/application/util/date_utils.ts
index 8f3215b6cd21..21adc0b4b9c6 100644
--- a/x-pack/plugins/ml/public/application/util/date_utils.ts
+++ b/x-pack/plugins/ml/public/application/util/date_utils.ts
@@ -8,7 +8,8 @@
 
 // @ts-ignore
 import { formatDate } from '@elastic/eui/lib/services/format';
-
+import dateMath from '@elastic/datemath';
+import { TimeRange } from '../../../../../../src/plugins/data/common';
 export function formatHumanReadableDate(ts: number) {
   return formatDate(ts, 'MMMM Do YYYY');
 }
@@ -20,3 +21,10 @@ export function formatHumanReadableDateTime(ts: number): string {
 export function formatHumanReadableDateTimeSeconds(ts: number) {
   return formatDate(ts, 'MMMM Do YYYY, HH:mm:ss');
 }
+
+export function validateTimeRange(time?: TimeRange): boolean {
+  if (!time) return false;
+  const momentDateFrom = dateMath.parse(time.from);
+  const momentDateTo = dateMath.parse(time.to);
+  return !!(momentDateFrom && momentDateFrom.isValid() && momentDateTo && momentDateTo.isValid());
+}
diff --git a/x-pack/plugins/ml/server/lib/register_settings.ts b/x-pack/plugins/ml/server/lib/register_settings.ts
index 38b1f5e3fc08..a9ee24fbb5ce 100644
--- a/x-pack/plugins/ml/server/lib/register_settings.ts
+++ b/x-pack/plugins/ml/server/lib/register_settings.ts
@@ -7,7 +7,13 @@
 import { CoreSetup } from 'kibana/server';
 import { i18n } from '@kbn/i18n';
 import { schema } from '@kbn/config-schema';
-import { FILE_DATA_VISUALIZER_MAX_FILE_SIZE } from '../../common/constants/settings';
+import {
+  FILE_DATA_VISUALIZER_MAX_FILE_SIZE,
+  ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
+  ANOMALY_DETECTION_ENABLE_TIME_RANGE,
+  DEFAULT_AD_RESULTS_TIME_FILTER,
+  DEFAULT_ENABLE_AD_RESULTS_TIME_FILTER,
+} from '../../common/constants/settings';
 import { MAX_FILE_SIZE } from '../../common/constants/file_datavisualizer';
 
 export function registerKibanaSettings(coreSetup: CoreSetup) {
@@ -30,5 +36,40 @@ export function registerKibanaSettings(coreSetup: CoreSetup) {
         }),
       },
     },
+    [ANOMALY_DETECTION_ENABLE_TIME_RANGE]: {
+      name: i18n.translate('xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeName', {
+        defaultMessage: 'Enable time filter defaults for anomaly detection results',
+      }),
+      value: DEFAULT_ENABLE_AD_RESULTS_TIME_FILTER,
+      schema: schema.boolean(),
+      description: i18n.translate(
+        'xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeDesc',
+        {
+          defaultMessage:
+            'Use the default time filter in the Single Metric Viewer and Anomaly Explorer. If not enabled, the results for the full time range of the job are displayed.',
+        }
+      ),
+      category: ['Machine Learning'],
+    },
+    [ANOMALY_DETECTION_DEFAULT_TIME_RANGE]: {
+      name: i18n.translate('xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeName', {
+        defaultMessage: 'Time filter defaults for anomaly detection results',
+      }),
+      type: 'json',
+      value: JSON.stringify(DEFAULT_AD_RESULTS_TIME_FILTER, null, 2),
+      description: i18n.translate(
+        'xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeDesc',
+        {
+          defaultMessage:
+            'The time filter selection to use when viewing anomaly detection job results.',
+        }
+      ),
+      schema: schema.object({
+        from: schema.string(),
+        to: schema.string(),
+      }),
+      requiresPageReload: true,
+      category: ['Machine Learning'],
+    },
   });
 }
diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts
index 420fa8347cde..e0d018869cef 100644
--- a/x-pack/plugins/reporting/server/plugin.test.ts
+++ b/x-pack/plugins/reporting/server/plugin.test.ts
@@ -86,6 +86,15 @@ describe('Reporting Plugin', () => {
     expect(plugin.start(coreStart, pluginStart)).not.toHaveProperty('then');
   });
 
+  it('registers an advanced setting for PDF logos', async () => {
+    const plugin = new ReportingPlugin(initContext);
+    plugin.setup(coreSetup, pluginSetup);
+    expect(coreSetup.uiSettings.register).toHaveBeenCalled();
+    expect(coreSetup.uiSettings.register.mock.calls[0][0]).toHaveProperty(
+      'xpackReporting:customPdfLogo'
+    );
+  });
+
   it('logs start issues', async () => {
     const plugin = new ReportingPlugin(initContext);
     // @ts-ignore overloading error logger
diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts
index 20e22c2db00e..8c0e352aa06c 100644
--- a/x-pack/plugins/reporting/server/plugin.ts
+++ b/x-pack/plugins/reporting/server/plugin.ts
@@ -4,16 +4,21 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
+import { schema } from '@kbn/config-schema';
+import { i18n } from '@kbn/i18n';
 import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server';
+import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from '../common/constants';
 import { ReportingCore } from './';
 import { initializeBrowserDriverFactory } from './browsers';
 import { buildConfig, ReportingConfigType } from './config';
-import { createQueueFactory, LevelLogger, runValidations, ReportingStore } from './lib';
+import { createQueueFactory, LevelLogger, ReportingStore, runValidations } from './lib';
 import { registerRoutes } from './routes';
 import { setFieldFormats } from './services';
 import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types';
 import { registerReportingUsageCollector } from './usage';
 
+const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6);
+
 declare module 'src/core/server' {
   interface RequestHandlerContext {
     reporting?: ReportingStart | null;
@@ -34,7 +39,7 @@ export class ReportingPlugin
 
   public setup(core: CoreSetup, plugins: ReportingSetupDeps) {
     // prevent throwing errors in route handlers about async deps not being initialized
-    core.http.registerRouteHandlerContext('reporting', () => {
+    core.http.registerRouteHandlerContext(PLUGIN_ID, () => {
       if (this.reportingCore.pluginIsStarted()) {
         return {}; // ReportingStart contract
       } else {
@@ -42,6 +47,28 @@ export class ReportingPlugin
       }
     });
 
+    core.uiSettings.register({
+      [UI_SETTINGS_CUSTOM_PDF_LOGO]: {
+        name: i18n.translate('xpack.reporting.pdfFooterImageLabel', {
+          defaultMessage: 'PDF footer image',
+        }),
+        value: null,
+        description: i18n.translate('xpack.reporting.pdfFooterImageDescription', {
+          defaultMessage: `Custom image to use in the PDF's footer`,
+        }),
+        type: 'image',
+        schema: schema.nullable(schema.byteSize({ max: '200kb' })),
+        category: [PLUGIN_ID],
+        // Used client-side for size validation
+        validation: {
+          maxSize: {
+            length: kbToBase64Length(200),
+            description: '200 kB',
+          },
+        },
+      },
+    });
+
     const { elasticsearch, http } = core;
     const { licensing, security } = plugins;
     const { initializerContext: initContext, reportingCore } = this;
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
index a9f39d2db608..498b561a818f 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
@@ -283,6 +283,9 @@ export type Status = t.TypeOf<typeof status>;
 export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null });
 export type JobStatus = t.TypeOf<typeof job_status>;
 
+export const conflicts = t.keyof({ abort: null, proceed: null });
+export type Conflicts = t.TypeOf<typeof conflicts>;
+
 // TODO: Create a regular expression type or custom date math part type here
 export const to = t.string;
 export type To = t.TypeOf<typeof to>;
@@ -338,7 +341,7 @@ export const sortFieldOrUndefined = t.union([sort_field, t.undefined]);
 export type SortFieldOrUndefined = t.TypeOf<typeof sortFieldOrUndefined>;
 
 export const sort_order = t.keyof({ asc: null, desc: null });
-export type sortOrder = t.TypeOf<typeof sort_order>;
+export type SortOrder = t.TypeOf<typeof sort_order>;
 
 export const sortOrderOrUndefined = t.union([sort_order, t.undefined]);
 export type SortOrderOrUndefined = t.TypeOf<typeof sortOrderOrUndefined>;
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts
new file mode 100644
index 000000000000..abfbc3918964
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+export * from './add_prepackaged_rules_schema';
+export * from './create_rules_bulk_schema';
+export * from './create_rules_schema';
+export * from './export_rules_schema';
+export * from './find_rules_schema';
+export * from './import_rules_schema';
+export * from './patch_rules_bulk_schema';
+export * from './patch_rules_schema';
+export * from './query_rules_schema';
+export * from './query_signals_index_schema';
+export * from './set_signal_status_schema';
+export * from './update_rules_bulk_schema';
+export * from './update_rules_schema';
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.ts
index 1464896e5029..b039558d827b 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.ts
@@ -6,13 +6,14 @@
 
 import * as t from 'io-ts';
 
-import { signal_ids, signal_status_query, status } from '../common/schemas';
+import { conflicts, signal_ids, signal_status_query, status } from '../common/schemas';
 
 export const setSignalsStatusSchema = t.intersection([
   t.type({
     status,
   }),
   t.partial({
+    conflicts,
     signal_ids,
     query: signal_status_query,
   }),
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts
new file mode 100644
index 000000000000..6c22b8140e73
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+export * from './error_schema';
+export * from './find_rules_schema';
+export * from './import_rules_schema';
+export * from './prepackaged_rules_schema';
+export * from './prepackaged_rules_status_schema';
+export * from './rules_bulk_schema';
+export * from './rules_schema';
+export * from './type_timeline_only_schema';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
index 0576871a2bf8..b55226b08b80 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
@@ -80,10 +80,6 @@ export interface Explanation {
   details: Explanation[];
 }
 
-export interface TotalValue {
-  value: number;
-  relation: string;
-}
 export interface ShardsResponse {
   total: number;
   successful: number;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts
index 0fb0609b60ba..efdc96b33562 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts
@@ -19,14 +19,14 @@ import {
 } from '../../../common';
 import { RequestOptionsPaginated } from '../../';
 
-export interface AuthenticationsStrategyResponse extends IEsSearchResponse {
+export interface HostAuthenticationsStrategyResponse extends IEsSearchResponse {
   edges: AuthenticationsEdges[];
   totalCount: number;
   pageInfo: PageInfoPaginated;
   inspect?: Maybe<Inspect>;
 }
 
-export interface AuthenticationsRequestOptions extends RequestOptionsPaginated {
+export interface HostAuthenticationsRequestOptions extends RequestOptionsPaginated {
   defaultIndex: string[];
 }
 
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts
index 8ae41a101cee..902e9909cf72 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts
@@ -6,7 +6,7 @@
 
 import { CloudEcs } from '../../../../ecs/cloud';
 import { HostEcs, OsEcs } from '../../../../ecs/host';
-import { Maybe, SearchHit, TotalValue } from '../../../common';
+import { Hit, Hits, Maybe, SearchHit, StringOrNumber, TotalValue } from '../../../common';
 
 export enum HostPolicyResponseActionStatus {
   success = 'success',
@@ -98,3 +98,15 @@ export interface HostAggEsData extends SearchHit {
   sort: string[];
   aggregations: HostAggEsItem;
 }
+
+export interface HostHit extends Hit {
+  _source: {
+    '@timestamp'?: string;
+    host: HostEcs;
+  };
+  cursor?: string;
+  firstSeen?: string;
+  sort?: StringOrNumber[];
+}
+
+export type HostHits = Hits<number, HostHit>;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts
index 9cb43c91adfd..f5d46078fcea 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts
@@ -9,10 +9,12 @@ export * from './all';
 export * from './common';
 export * from './overview';
 export * from './first_last_seen';
+export * from './uncommon_processes';
 
 export enum HostsQueries {
   authentications = 'authentications',
   firstLastSeen = 'firstLastSeen',
   hosts = 'hosts',
   hostOverview = 'hostOverview',
+  uncommonProcesses = 'uncommonProcesses',
 }
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts
new file mode 100644
index 000000000000..28c0ccb7f6f4
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
+
+import { HostEcs } from '../../../../ecs/host';
+import { UserEcs } from '../../../../ecs/user';
+import {
+  RequestOptionsPaginated,
+  SortField,
+  CursorType,
+  Inspect,
+  Maybe,
+  PageInfoPaginated,
+  Hit,
+  TotalHit,
+  StringOrNumber,
+  Hits,
+} from '../../..';
+
+export interface HostUncommonProcessesRequestOptions extends RequestOptionsPaginated {
+  sort: SortField;
+  defaultIndex: string[];
+}
+
+export interface HostUncommonProcessesStrategyResponse extends IEsSearchResponse {
+  edges: UncommonProcessesEdges[];
+  totalCount: number;
+  pageInfo: PageInfoPaginated;
+  inspect?: Maybe<Inspect>;
+}
+
+export interface UncommonProcessesEdges {
+  node: UncommonProcessItem;
+  cursor: CursorType;
+}
+
+export interface UncommonProcessItem {
+  _id: string;
+  instances: number;
+  process: ProcessEcsFields;
+  hosts: HostEcs[];
+  user?: Maybe<UserEcs>;
+}
+
+export interface ProcessEcsFields {
+  hash?: Maybe<ProcessHashData>;
+  pid?: Maybe<number[]>;
+  name?: Maybe<string[]>;
+  ppid?: Maybe<number[]>;
+  args?: Maybe<string[]>;
+  entity_id?: Maybe<string[]>;
+  executable?: Maybe<string[]>;
+  title?: Maybe<string[]>;
+  thread?: Maybe<Thread>;
+  working_directory?: Maybe<string[]>;
+}
+
+export interface ProcessHashData {
+  md5?: Maybe<string[]>;
+  sha1?: Maybe<string[]>;
+  sha256?: Maybe<string[]>;
+}
+
+export interface Thread {
+  id?: Maybe<number[]>;
+  start?: Maybe<string[]>;
+}
+
+export interface UncommonProcessHit extends Hit {
+  total: TotalHit;
+  host: Array<{
+    id: string[] | undefined;
+    name: string[] | undefined;
+  }>;
+  _source: {
+    '@timestamp': string;
+    process: ProcessEcsFields;
+  };
+  cursor: string;
+  sort: StringOrNumber[];
+}
+
+export type ProcessHits = Hits<TotalHit, UncommonProcessHit>;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
index 85ffc6aa4c73..7721f2ae97d7 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
@@ -8,17 +8,17 @@ import { IEsSearchRequest } from '../../../../../../src/plugins/data/common';
 import { ESQuery } from '../../typed_json';
 import {
   HostOverviewStrategyResponse,
+  HostAuthenticationsRequestOptions,
+  HostAuthenticationsStrategyResponse,
   HostOverviewRequestOptions,
   HostFirstLastSeenStrategyResponse,
   HostFirstLastSeenRequestOptions,
   HostsQueries,
   HostsRequestOptions,
   HostsStrategyResponse,
+  HostUncommonProcessesStrategyResponse,
+  HostUncommonProcessesRequestOptions,
 } from './hosts';
-import {
-  AuthenticationsRequestOptions,
-  AuthenticationsStrategyResponse,
-} from './hosts/authentications';
 import {
   NetworkQueries,
   NetworkTlsStrategyResponse,
@@ -27,6 +27,8 @@ import {
   NetworkHttpRequestOptions,
   NetworkTopCountriesStrategyResponse,
   NetworkTopCountriesRequestOptions,
+  NetworkTopNFlowStrategyResponse,
+  NetworkTopNFlowRequestOptions,
 } from './network';
 import {
   DocValueFields,
@@ -66,15 +68,19 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
   : T extends HostsQueries.hostOverview
   ? HostOverviewStrategyResponse
   : T extends HostsQueries.authentications
-  ? AuthenticationsStrategyResponse
+  ? HostAuthenticationsStrategyResponse
   : T extends HostsQueries.firstLastSeen
   ? HostFirstLastSeenStrategyResponse
+  : T extends HostsQueries.uncommonProcesses
+  ? HostUncommonProcessesStrategyResponse
   : T extends NetworkQueries.tls
   ? NetworkTlsStrategyResponse
   : T extends NetworkQueries.http
   ? NetworkHttpStrategyResponse
   : T extends NetworkQueries.topCountries
   ? NetworkTopCountriesStrategyResponse
+  : T extends NetworkQueries.topNFlow
+  ? NetworkTopNFlowStrategyResponse
   : never;
 
 export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQueries.hosts
@@ -82,13 +88,17 @@ export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQu
   : T extends HostsQueries.hostOverview
   ? HostOverviewRequestOptions
   : T extends HostsQueries.authentications
-  ? AuthenticationsRequestOptions
+  ? HostAuthenticationsRequestOptions
   : T extends HostsQueries.firstLastSeen
   ? HostFirstLastSeenRequestOptions
+  : T extends HostsQueries.uncommonProcesses
+  ? HostUncommonProcessesRequestOptions
   : T extends NetworkQueries.tls
   ? NetworkTlsRequestOptions
   : T extends NetworkQueries.http
   ? NetworkHttpRequestOptions
   : T extends NetworkQueries.topCountries
   ? NetworkTopCountriesRequestOptions
+  : T extends NetworkQueries.topNFlow
+  ? NetworkTopNFlowRequestOptions
   : never;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
index a6ae956a4218..66676569b3c9 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
@@ -4,7 +4,28 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
+import { GeoEcs } from '../../../../ecs/geo';
+import { Maybe } from '../../..';
+
+export enum NetworkTopTablesFields {
+  bytes_in = 'bytes_in',
+  bytes_out = 'bytes_out',
+  flows = 'flows',
+  destination_ips = 'destination_ips',
+  source_ips = 'source_ips',
+}
+
 export enum FlowTargetSourceDest {
   destination = 'destination',
   source = 'source',
 }
+
+export interface TopNetworkTablesEcsField {
+  bytes_in?: Maybe<number>;
+  bytes_out?: Maybe<number>;
+}
+
+export interface GeoItem {
+  geo?: Maybe<GeoEcs>;
+  flowTarget?: Maybe<FlowTargetSourceDest>;
+}
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts
index ac5e6fdacc94..2992ee32f8ac 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts
@@ -8,9 +8,11 @@ export * from './common';
 export * from './http';
 export * from './tls';
 export * from './top_countries';
+export * from './top_n_flow';
 
 export enum NetworkQueries {
   http = 'http',
   tls = 'tls',
   topCountries = 'topCountries',
+  topNFlow = 'topNFlow',
 }
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts
index 3188a26dd69f..f499db82d647 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts
@@ -5,18 +5,14 @@
  */
 
 import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
-import { GeoEcs } from '../../../../ecs/geo';
 import { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common';
 import { RequestOptionsPaginated } from '../..';
-import { FlowTargetSourceDest } from '../common';
-
-export enum NetworkTopTablesFields {
-  bytes_in = 'bytes_in',
-  bytes_out = 'bytes_out',
-  flows = 'flows',
-  destination_ips = 'destination_ips',
-  source_ips = 'source_ips',
-}
+import {
+  GeoItem,
+  FlowTargetSourceDest,
+  NetworkTopTablesFields,
+  TopNetworkTablesEcsField,
+} from '../common';
 
 export enum NetworkDnsFields {
   dnsName = 'dnsName',
@@ -33,11 +29,6 @@ export enum FlowTarget {
   source = 'source',
 }
 
-export interface GeoItem {
-  geo?: Maybe<GeoEcs>;
-  flowTarget?: Maybe<FlowTargetSourceDest>;
-}
-
 export interface TopCountriesItemSource {
   country?: Maybe<string>;
   destination_ips?: Maybe<number>;
@@ -79,11 +70,6 @@ export interface TopCountriesItemDestination {
   source_ips?: Maybe<number>;
 }
 
-export interface TopNetworkTablesEcsField {
-  bytes_in?: Maybe<number>;
-  bytes_out?: Maybe<number>;
-}
-
 export interface NetworkTopCountriesBuckets {
   country: string;
   key: string;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_n_flow/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_n_flow/index.ts
new file mode 100644
index 000000000000..d6be2d29c6ed
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_n_flow/index.ts
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
+import {
+  GeoItem,
+  FlowTargetSourceDest,
+  TopNetworkTablesEcsField,
+  NetworkTopTablesFields,
+} from '../common';
+import {
+  CursorType,
+  Inspect,
+  Maybe,
+  PageInfoPaginated,
+  TotalValue,
+  GenericBuckets,
+} from '../../../common';
+import { RequestOptionsPaginated } from '../..';
+
+export interface NetworkTopNFlowRequestOptions
+  extends RequestOptionsPaginated<NetworkTopTablesFields> {
+  flowTarget: FlowTargetSourceDest;
+  ip?: Maybe<string>;
+}
+
+export interface NetworkTopNFlowStrategyResponse extends IEsSearchResponse {
+  edges: NetworkTopNFlowEdges[];
+  totalCount: number;
+  pageInfo: PageInfoPaginated;
+  inspect?: Maybe<Inspect>;
+}
+
+export interface NetworkTopNFlowEdges {
+  node: NetworkTopNFlowItem;
+  cursor: CursorType;
+}
+
+export interface NetworkTopNFlowItem {
+  _id?: Maybe<string>;
+  source?: Maybe<TopNFlowItemSource>;
+  destination?: Maybe<TopNFlowItemDestination>;
+  network?: Maybe<TopNetworkTablesEcsField>;
+}
+
+export interface TopNFlowItemSource {
+  autonomous_system?: Maybe<AutonomousSystemItem>;
+  domain?: Maybe<string[]>;
+  ip?: Maybe<string>;
+  location?: Maybe<GeoItem>;
+  flows?: Maybe<number>;
+  destination_ips?: Maybe<number>;
+}
+
+export interface AutonomousSystemItem {
+  name?: Maybe<string>;
+  number?: Maybe<number>;
+}
+
+export interface TopNFlowItemDestination {
+  autonomous_system?: Maybe<AutonomousSystemItem>;
+  domain?: Maybe<string[]>;
+  ip?: Maybe<string>;
+  location?: Maybe<GeoItem>;
+  flows?: Maybe<number>;
+  source_ips?: Maybe<number>;
+}
+
+export interface AutonomousSystemHit<T> {
+  doc_count: number;
+  top_as: {
+    hits: {
+      total: TotalValue | number;
+      max_score: number | null;
+      hits: Array<{
+        _source: T;
+        sort?: [number];
+        _index?: string;
+        _type?: string;
+        _id?: string;
+        _score?: number | null;
+      }>;
+    };
+  };
+}
+
+export interface NetworkTopNFlowBuckets {
+  key: string;
+  autonomous_system: AutonomousSystemHit<object>;
+  bytes_in: {
+    value: number;
+  };
+  bytes_out: {
+    value: number;
+  };
+  domain: {
+    buckets: GenericBuckets[];
+  };
+  location: LocationHit<object>;
+  flows: number;
+  destination_ips?: number;
+  source_ips?: number;
+}
+
+export interface LocationHit<T> {
+  doc_count: number;
+  top_geo: {
+    hits: {
+      total: TotalValue | number;
+      max_score: number | null;
+      hits: Array<{
+        _source: T;
+        sort?: [number];
+        _index?: string;
+        _type?: string;
+        _id?: string;
+        _score?: number | null;
+      }>;
+    };
+  };
+}
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
index ba1de0e40e27..d9d9fde8fc8c 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
@@ -58,6 +58,8 @@ import {
   createAndActivateRule,
   fillAboutRuleAndContinue,
   fillDefineCustomRuleWithImportedQueryAndContinue,
+  expectDefineFormToRepopulateAndContinue,
+  expectAboutFormToRepopulateAndContinue,
 } from '../tasks/create_new_rule';
 import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
 import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
@@ -82,6 +84,8 @@ describe('Detection rules, custom', () => {
     goToCreateNewRule();
     fillDefineCustomRuleWithImportedQueryAndContinue(newRule);
     fillAboutRuleAndContinue(newRule);
+    expectDefineFormToRepopulateAndContinue(newRule);
+    expectAboutFormToRepopulateAndContinue(newRule);
     createAndActivateRule();
 
     cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)');
diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts
index 83ace877cd51..397d0c014217 100644
--- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts
@@ -6,6 +6,8 @@
 
 export const ABOUT_CONTINUE_BTN = '[data-test-subj="about-continue"]';
 
+export const ABOUT_EDIT_BUTTON = '[data-test-subj="edit-about-rule"]';
+
 export const ADD_FALSE_POSITIVE_BTN =
   '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text';
 
@@ -26,6 +28,8 @@ export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]';
 
 export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]';
 
+export const DEFINE_EDIT_BUTTON = '[data-test-subj="edit-define-rule"]';
+
 export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK =
   '[data-test-subj="importQueryFromSavedTimeline"]';
 
diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
index 1cce72a48e0f..3fa300ce9d8d 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts
@@ -48,6 +48,8 @@ import {
   THRESHOLD_FIELD_SELECTION,
   THRESHOLD_INPUT_AREA,
   THRESHOLD_TYPE,
+  DEFINE_EDIT_BUTTON,
+  ABOUT_EDIT_BUTTON,
 } from '../screens/create_new_rule';
 import { TIMELINE } from '../screens/timeline';
 
@@ -175,6 +177,20 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = (
   cy.get(CUSTOM_QUERY_INPUT).should('not.exist');
 };
 
+export const expectDefineFormToRepopulateAndContinue = (rule: CustomRule) => {
+  cy.get(DEFINE_EDIT_BUTTON).click();
+  cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery);
+  cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
+  cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist');
+};
+
+export const expectAboutFormToRepopulateAndContinue = (rule: CustomRule) => {
+  cy.get(ABOUT_EDIT_BUTTON).click();
+  cy.get(RULE_NAME_INPUT).invoke('val').should('eq', rule.name);
+  cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true });
+  cy.get(ABOUT_CONTINUE_BTN).should('not.exist');
+};
+
 export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => {
   const thresholdField = 0;
   const threshold = 1;
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
index c46eb1b6b59c..c1befabdd780 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
@@ -26,6 +26,7 @@ import {
   CreateExceptionListItemSchema,
   ExceptionListType,
 } from '../../../../../public/lists_plugin_deps';
+import * as i18nCommon from '../../../translations';
 import * as i18n from './translations';
 import * as sharedI18n from '../translations';
 import { TimelineNonEcsData, Ecs } from '../../../../graphql/types';
@@ -49,6 +50,7 @@ import {
 } from '../helpers';
 import { ErrorInfo, ErrorCallout } from '../error_callout';
 import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
+import { ExceptionsBuilderExceptionItem } from '../types';
 
 export interface AddExceptionModalBaseProps {
   ruleName: string;
@@ -117,7 +119,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
     Array<ExceptionListItemSchema | CreateExceptionListItemSchema>
   >([]);
   const [fetchOrCreateListError, setFetchOrCreateListError] = useState<ErrorInfo | null>(null);
-  const { addError, addSuccess } = useAppToasts();
+  const { addError, addSuccess, addWarning } = useAppToasts();
   const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
   const [
     { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns },
@@ -129,16 +131,26 @@ export const AddExceptionModal = memo(function AddExceptionModal({
   );
 
   const onError = useCallback(
-    (error: Error) => {
+    (error: Error): void => {
       addError(error, { title: i18n.ADD_EXCEPTION_ERROR });
       onCancel();
     },
     [addError, onCancel]
   );
-  const onSuccess = useCallback(() => {
-    addSuccess(i18n.ADD_EXCEPTION_SUCCESS);
-    onConfirm(shouldCloseAlert, shouldBulkCloseAlert);
-  }, [addSuccess, onConfirm, shouldBulkCloseAlert, shouldCloseAlert]);
+
+  const onSuccess = useCallback(
+    (updated: number, conflicts: number): void => {
+      addSuccess(i18n.ADD_EXCEPTION_SUCCESS);
+      onConfirm(shouldCloseAlert, shouldBulkCloseAlert);
+      if (conflicts > 0) {
+        addWarning({
+          title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts),
+          text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts),
+        });
+      }
+    },
+    [addSuccess, addWarning, onConfirm, shouldBulkCloseAlert, shouldCloseAlert]
+  );
 
   const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException(
     {
@@ -153,7 +165,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
       exceptionItems,
     }: {
       exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>;
-    }) => {
+    }): void => {
       setExceptionItemsToAdd(exceptionItems);
     },
     [setExceptionItemsToAdd]
@@ -186,7 +198,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
   );
 
   const handleFetchOrCreateExceptionListError = useCallback(
-    (error: Error, statusCode: number | null, message: string | null) => {
+    (error: Error, statusCode: number | null, message: string | null): void => {
       setFetchOrCreateListError({
         reason: error.message,
         code: statusCode,
@@ -205,7 +217,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
     onSuccess: handleRuleChange,
   });
 
-  const initialExceptionItems = useMemo(() => {
+  const initialExceptionItems = useMemo((): ExceptionsBuilderExceptionItem[] => {
     if (exceptionListType === 'endpoint' && alertData !== undefined && ruleExceptionList) {
       return defaultEndpointExceptionItems(
         exceptionListType,
@@ -218,7 +230,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
     }
   }, [alertData, exceptionListType, ruleExceptionList, ruleName]);
 
-  useEffect(() => {
+  useEffect((): void => {
     if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) {
       setShouldDisableBulkClose(
         entryHasListType(exceptionItemsToAdd) ||
@@ -234,34 +246,34 @@ export const AddExceptionModal = memo(function AddExceptionModal({
     signalIndexPatterns,
   ]);
 
-  useEffect(() => {
+  useEffect((): void => {
     if (shouldDisableBulkClose === true) {
       setShouldBulkCloseAlert(false);
     }
   }, [shouldDisableBulkClose]);
 
   const onCommentChange = useCallback(
-    (value: string) => {
+    (value: string): void => {
       setComment(value);
     },
     [setComment]
   );
 
   const onCloseAlertCheckboxChange = useCallback(
-    (event: React.ChangeEvent<HTMLInputElement>) => {
+    (event: React.ChangeEvent<HTMLInputElement>): void => {
       setShouldCloseAlert(event.currentTarget.checked);
     },
     [setShouldCloseAlert]
   );
 
   const onBulkCloseAlertCheckboxChange = useCallback(
-    (event: React.ChangeEvent<HTMLInputElement>) => {
+    (event: React.ChangeEvent<HTMLInputElement>): void => {
       setShouldBulkCloseAlert(event.currentTarget.checked);
     },
     [setShouldBulkCloseAlert]
   );
 
-  const retrieveAlertOsTypes = useCallback(() => {
+  const retrieveAlertOsTypes = useCallback((): string[] => {
     const osDefaults = ['windows', 'macos'];
     if (alertData) {
       const osTypes = getMappedNonEcsValue({
@@ -276,7 +288,9 @@ export const AddExceptionModal = memo(function AddExceptionModal({
     return osDefaults;
   }, [alertData]);
 
-  const enrichExceptionItems = useCallback(() => {
+  const enrichExceptionItems = useCallback((): Array<
+    ExceptionListItemSchema | CreateExceptionListItemSchema
+  > => {
     let enriched: Array<ExceptionListItemSchema | CreateExceptionListItemSchema> = [];
     enriched =
       comment !== ''
@@ -289,7 +303,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
     return enriched;
   }, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]);
 
-  const onAddExceptionConfirm = useCallback(() => {
+  const onAddExceptionConfirm = useCallback((): void => {
     if (addOrUpdateExceptionItems !== null) {
       const alertIdToClose = shouldCloseAlert && alertData ? alertData.ecsData._id : undefined;
       const bulkCloseIndex =
@@ -306,7 +320,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
   ]);
 
   const isSubmitButtonDisabled = useMemo(
-    () =>
+    (): boolean =>
       fetchOrCreateListError != null ||
       exceptionItemsToAdd.every((item) => item.entries.length === 0),
     [fetchOrCreateListError, exceptionItemsToAdd]
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx
index 46923e07d225..2398f8d799c2 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx
@@ -49,7 +49,7 @@ describe('useAddOrUpdateException', () => {
   const onError = jest.fn();
   const onSuccess = jest.fn();
   const alertIdToClose = 'idToClose';
-  const bulkCloseIndex = ['.signals'];
+  const bulkCloseIndex = ['.custom'];
   const itemsToAdd: CreateExceptionListItemSchema[] = [
     {
       ...getCreateExceptionListItemSchemaMock(),
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx
index be289b0e85e6..dbd634e97a32 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx
@@ -5,6 +5,7 @@
  */
 
 import { useEffect, useRef, useState, useCallback } from 'react';
+import { UpdateDocumentByQueryResponse } from 'elasticsearch';
 import { HttpStart } from '../../../../../../../src/core/public';
 
 import {
@@ -43,7 +44,7 @@ export type ReturnUseAddOrUpdateException = [
 export interface UseAddOrUpdateExceptionProps {
   http: HttpStart;
   onError: (arg: Error, code: number | null, message: string | null) => void;
-  onSuccess: () => void;
+  onSuccess: (updated: number, conficts: number) => void;
 }
 
 /**
@@ -122,8 +123,10 @@ export const useAddOrUpdateException = ({
     ) => {
       try {
         setIsLoading(true);
-        if (alertIdToClose !== null && alertIdToClose !== undefined) {
-          await updateAlertStatus({
+        let alertIdResponse: UpdateDocumentByQueryResponse | undefined;
+        let bulkResponse: UpdateDocumentByQueryResponse | undefined;
+        if (alertIdToClose != null) {
+          alertIdResponse = await updateAlertStatus({
             query: getUpdateAlertsQuery([alertIdToClose]),
             status: 'closed',
             signal: abortCtrl.signal,
@@ -139,7 +142,8 @@ export const useAddOrUpdateException = ({
             prepareExceptionItemsForBulkClose(exceptionItemsToAddOrUpdate),
             false
           );
-          await updateAlertStatus({
+
+          bulkResponse = await updateAlertStatus({
             query: {
               query: filter,
             },
@@ -150,9 +154,18 @@ export const useAddOrUpdateException = ({
 
         await addOrUpdateItems(exceptionItemsToAddOrUpdate);
 
+        // NOTE: there could be some overlap here... it's possible that the first response had conflicts
+        // but that the alert was closed in the second call. In this case, a conflict will be reported even
+        // though it was already resolved. I'm not sure that there's an easy way to solve this, but it should
+        // have minimal impact on the user... they'd see a warning that indicates a possible conflict, but the
+        // state of the alerts and their representation in the UI would be consistent.
+        const updated = (alertIdResponse?.updated ?? 0) + (bulkResponse?.updated ?? 0);
+        const conflicts =
+          alertIdResponse?.version_conflicts ?? 0 + (bulkResponse?.version_conflicts ?? 0);
+
         if (isSubscribed) {
           setIsLoading(false);
-          onSuccess();
+          onSuccess(updated, conflicts);
         }
       } catch (error) {
         if (isSubscribed) {
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
index e0e629793952..da43d0c51099 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
@@ -14,13 +14,16 @@ jest.mock('../lib/kibana');
 describe('useDeleteList', () => {
   let addErrorMock: jest.Mock;
   let addSuccessMock: jest.Mock;
+  let addWarningMock: jest.Mock;
 
   beforeEach(() => {
     addErrorMock = jest.fn();
     addSuccessMock = jest.fn();
+    addWarningMock = jest.fn();
     (useToasts as jest.Mock).mockImplementation(() => ({
       addError: addErrorMock,
       addSuccess: addSuccessMock,
+      addWarning: addWarningMock,
     }));
   });
 
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
index bc59d8710005..ae811e740073 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
@@ -10,7 +10,7 @@ import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/cor
 import { useToasts } from '../lib/kibana';
 import { isAppError, AppError } from '../utils/api';
 
-export type UseAppToasts = Pick<ToastsStart, 'addSuccess'> & {
+export type UseAppToasts = Pick<ToastsStart, 'addSuccess' | 'addWarning'> & {
   api: ToastsStart;
   addError: (error: unknown, options: ErrorToastOptions) => Toast;
 };
@@ -19,6 +19,7 @@ export const useAppToasts = (): UseAppToasts => {
   const toasts = useToasts();
   const addError = useRef(toasts.addError.bind(toasts)).current;
   const addSuccess = useRef(toasts.addSuccess.bind(toasts)).current;
+  const addWarning = useRef(toasts.addWarning.bind(toasts)).current;
 
   const addAppError = useCallback(
     (error: AppError, options: ErrorToastOptions) =>
@@ -44,5 +45,5 @@ export const useAppToasts = (): UseAppToasts => {
     [addAppError, addError]
   );
 
-  return { api: toasts, addError: _addError, addSuccess };
+  return { api: toasts, addError: _addError, addSuccess, addWarning };
 };
diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.ts
index 075f06084384..b4fb307a62b6 100644
--- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.ts
@@ -19,12 +19,11 @@ export interface WithKibanaProps {
   kibana: KibanaContext;
 }
 
-// eslint-disable-next-line react-hooks/rules-of-hooks
-const typedUseKibana = () => useKibana<StartServices>();
+const useTypedKibana = () => useKibana<StartServices>();
 
 export {
   KibanaContextProvider,
-  typedUseKibana as useKibana,
+  useTypedKibana as useKibana,
   useUiSetting,
   useUiSetting$,
   withKibana,
diff --git a/x-pack/plugins/security_solution/public/common/translations.ts b/x-pack/plugins/security_solution/public/common/translations.ts
index 3b94ac895949..c4a9540f6291 100644
--- a/x-pack/plugins/security_solution/public/common/translations.ts
+++ b/x-pack/plugins/security_solution/public/common/translations.ts
@@ -61,3 +61,17 @@ export const EMPTY_ACTION_ENDPOINT_DESCRIPTION = i18n.translate(
       'Protect your hosts with threat prevention, detection, and deep security data visibility.',
   }
 );
+
+export const UPDATE_ALERT_STATUS_FAILED = (conflicts: number) =>
+  i18n.translate('xpack.securitySolution.pages.common.updateAlertStatusFailed', {
+    values: { conflicts },
+    defaultMessage:
+      'Failed to update { conflicts } {conflicts, plural, =1 {alert} other {alerts}}.',
+  });
+
+export const UPDATE_ALERT_STATUS_FAILED_DETAILED = (updated: number, conflicts: number) =>
+  i18n.translate('xpack.securitySolution.pages.common.updateAlertStatusFailedDetailed', {
+    values: { updated, conflicts },
+    defaultMessage: `{ updated } {updated, plural, =1 {alert was} other {alerts were}} updated successfully, but { conflicts } failed to update
+         because { conflicts, plural, =1 {it was} other {they were}} already being modified.`,
+  });
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
index 3545bfd91e55..972a8aa4b087 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
@@ -9,6 +9,7 @@
 import dateMath from '@elastic/datemath';
 import { get, getOr, isEmpty, find } from 'lodash/fp';
 import moment from 'moment';
+import { i18n } from '@kbn/i18n';
 
 import { TimelineId } from '../../../../common/types/timeline';
 import { updateAlertStatus } from '../../containers/detection_engine/alerts/api';
@@ -83,7 +84,18 @@ export const updateAlertStatusAction = async ({
     // TODO: Only delete those that were successfully updated from updatedRules
     setEventsDeleted({ eventIds: alertIds, isDeleted: true });
 
-    onAlertStatusUpdateSuccess(response.updated, selectedStatus);
+    if (response.version_conflicts > 0 && alertIds.length === 1) {
+      throw new Error(
+        i18n.translate(
+          'xpack.securitySolution.detectionEngine.alerts.updateAlertStatusFailedSingleAlert',
+          {
+            defaultMessage: 'Failed to update alert because it was already being modified.',
+          }
+        )
+      );
+    }
+
+    onAlertStatusUpdateSuccess(response.updated, response.version_conflicts, selectedStatus);
   } catch (error) {
     onAlertStatusUpdateFailure(selectedStatus, error);
   } finally {
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
index 63e1c8aca908..0416b3d2a459 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
@@ -9,10 +9,10 @@ import { isEmpty } from 'lodash/fp';
 import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import { connect, ConnectedProps } from 'react-redux';
 import { Dispatch } from 'redux';
-
 import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
 import { Filter, esQuery } from '../../../../../../../src/plugins/data/public';
 import { TimelineIdLiteral } from '../../../../common/types/timeline';
+import { useAppToasts } from '../../../common/hooks/use_app_toasts';
 import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns';
 import { StatefulEventsViewer } from '../../../common/components/events_viewer';
 import { HeaderSection } from '../../../common/components/header_section';
@@ -32,6 +32,7 @@ import {
 } from './default_config';
 import { FILTER_OPEN, AlertsTableFilterGroup } from './alerts_filter_group';
 import { AlertsUtilityBar } from './alerts_utility_bar';
+import * as i18nCommon from '../../../common/translations';
 import * as i18n from './translations';
 import {
   SetEventsDeletedProps,
@@ -90,6 +91,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
   );
   const kibana = useKibana();
   const [, dispatchToaster] = useStateToaster();
+  const { addWarning } = useAppToasts();
   const { initializeTimeline, setSelectAll, setIndexToAdd } = useManageTimeline();
 
   const getGlobalQuery = useCallback(
@@ -130,21 +132,29 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
   );
 
   const onAlertStatusUpdateSuccess = useCallback(
-    (count: number, status: Status) => {
-      let title: string;
-      switch (status) {
-        case 'closed':
-          title = i18n.CLOSED_ALERT_SUCCESS_TOAST(count);
-          break;
-        case 'open':
-          title = i18n.OPENED_ALERT_SUCCESS_TOAST(count);
-          break;
-        case 'in-progress':
-          title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(count);
+    (updated: number, conflicts: number, status: Status) => {
+      if (conflicts > 0) {
+        // Partial failure
+        addWarning({
+          title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts),
+          text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts),
+        });
+      } else {
+        let title: string;
+        switch (status) {
+          case 'closed':
+            title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated);
+            break;
+          case 'open':
+            title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated);
+            break;
+          case 'in-progress':
+            title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(updated);
+        }
+        displaySuccessToast(title, dispatchToaster);
       }
-      displaySuccessToast(title, dispatchToaster);
     },
-    [dispatchToaster]
+    [addWarning, dispatchToaster]
   );
 
   const onAlertStatusUpdateFailure = useCallback(
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
index 216ed0cbe264..cbf0e08fef5c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
@@ -15,6 +15,7 @@ import {
 } from '@elastic/eui';
 import styled from 'styled-components';
 
+import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
 import { TimelineId } from '../../../../../common/types/timeline';
 import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants';
 import { Status, Type } from '../../../../../common/detection_engine/schemas/common/schemas';
@@ -32,6 +33,7 @@ import {
   AddExceptionModalBaseProps,
 } from '../../../../common/components/exceptions/add_exception_modal';
 import { getMappedNonEcsValue } from '../../../../common/components/exceptions/helpers';
+import * as i18nCommon from '../../../../common/translations';
 import * as i18n from '../translations';
 import {
   useStateToaster,
@@ -72,6 +74,8 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
   );
   const eventId = ecsRowData._id;
 
+  const { addWarning } = useAppToasts();
+
   const onButtonClick = useCallback(() => {
     setPopover(!isPopoverOpen);
   }, [isPopoverOpen]);
@@ -124,22 +128,30 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
   );
 
   const onAlertStatusUpdateSuccess = useCallback(
-    (count: number, newStatus: Status) => {
-      let title: string;
-      switch (newStatus) {
-        case 'closed':
-          title = i18n.CLOSED_ALERT_SUCCESS_TOAST(count);
-          break;
-        case 'open':
-          title = i18n.OPENED_ALERT_SUCCESS_TOAST(count);
-          break;
-        case 'in-progress':
-          title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(count);
+    (updated: number, conflicts: number, newStatus: Status) => {
+      if (conflicts > 0) {
+        // Partial failure
+        addWarning({
+          title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts),
+          text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts),
+        });
+      } else {
+        let title: string;
+        switch (newStatus) {
+          case 'closed':
+            title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated);
+            break;
+          case 'open':
+            title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated);
+            break;
+          case 'in-progress':
+            title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(updated);
+        }
+        displaySuccessToast(title, dispatchToaster);
       }
-      displaySuccessToast(title, dispatchToaster);
       setAlertStatus(newStatus);
     },
-    [dispatchToaster]
+    [dispatchToaster, addWarning]
   );
 
   const onAlertStatusUpdateFailure = useCallback(
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts
index d8ba0ab2d40b..f8b3cd6af8b8 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts
@@ -44,7 +44,7 @@ export interface UpdateAlertStatusActionProps {
   selectedStatus: Status;
   setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void;
   setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void;
-  onAlertStatusUpdateSuccess: (count: number, status: Status) => void;
+  onAlertStatusUpdateSuccess: (updated: number, conflicts: number, status: Status) => void;
   onAlertStatusUpdateFailure: (status: Status, error: Error) => void;
 }
 
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx
index 8b3d05ce5a57..8179e5865e4e 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx
@@ -7,7 +7,7 @@ import React from 'react';
 import { shallow, mount } from 'enzyme';
 
 import {
-  StepRuleDescriptionComponent,
+  StepRuleDescription,
   addFilterStateIfNotThere,
   buildListItems,
   getDescriptionItem,
@@ -52,24 +52,24 @@ describe('description_step', () => {
     mockAboutStep = mockAboutStepRule();
   });
 
-  describe('StepRuleDescriptionComponent', () => {
+  describe('StepRuleDescription', () => {
     test('renders tow columns when "columns" is "multi"', () => {
       const wrapper = shallow(
-        <StepRuleDescriptionComponent columns="multi" data={mockAboutStep} schema={schema} />
+        <StepRuleDescription columns="multi" data={mockAboutStep} schema={schema} />
       );
       expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2);
     });
 
     test('renders single column when "columns" is "single"', () => {
       const wrapper = shallow(
-        <StepRuleDescriptionComponent columns="single" data={mockAboutStep} schema={schema} />
+        <StepRuleDescription columns="single" data={mockAboutStep} schema={schema} />
       );
       expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1);
     });
 
     test('renders one column with title and description split when "columns" is "singleSplit', () => {
       const wrapper = shallow(
-        <StepRuleDescriptionComponent columns="singleSplit" data={mockAboutStep} schema={schema} />
+        <StepRuleDescription columns="singleSplit" data={mockAboutStep} schema={schema} />
       );
       expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1);
       expect(
@@ -299,7 +299,6 @@ describe('description_step', () => {
     describe('queryBar', () => {
       test('returns array of ListItems when queryBar exist', () => {
         const mockQueryBar = {
-          isNew: false,
           queryBar: {
             query: {
               query: 'user.name: root or user.name: admin',
@@ -369,7 +368,6 @@ describe('description_step', () => {
     describe('threshold', () => {
       test('returns threshold description when threshold exist and field is empty', () => {
         const mockThreshold = {
-          isNew: false,
           threshold: {
             field: [''],
             value: 100,
@@ -391,7 +389,6 @@ describe('description_step', () => {
 
       test('returns threshold description when threshold exist and field is set', () => {
         const mockThreshold = {
-          isNew: false,
           threshold: {
             field: ['user.name'],
             value: 100,
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
index cf27fa97b147..99e36669f78b 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
@@ -37,7 +37,6 @@ import {
   buildRuleTypeDescription,
   buildThresholdDescription,
 } from './helpers';
-import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs';
 import { buildMlJobDescription } from './ml_job_description';
 import { buildActionsDescription } from './actions_description';
 import { buildThrottleDescription } from './throttle_description';
@@ -52,22 +51,21 @@ const DescriptionListContainer = styled(EuiDescriptionList)`
   }
 `;
 
-interface StepRuleDescriptionProps {
+interface StepRuleDescriptionProps<T> {
   columns?: 'multi' | 'single' | 'singleSplit';
   data: unknown;
   indexPatterns?: IIndexPattern;
-  schema: FormSchema;
+  schema: FormSchema<T>;
 }
 
-export const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> = ({
+export const StepRuleDescriptionComponent = <T,>({
   data,
   columns = 'multi',
   indexPatterns,
   schema,
-}) => {
+}: StepRuleDescriptionProps<T>) => {
   const kibana = useKibana();
   const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings));
-  const { jobs } = useSecurityJobs(false);
 
   const keys = Object.keys(schema);
   const listItems = keys.reduce((acc: ListItems[], key: string) => {
@@ -76,8 +74,7 @@ export const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> =
         ...acc,
         buildMlJobDescription(
           get(key, data) as string,
-          (get(key, schema) as { label: string }).label,
-          jobs
+          (get(key, schema) as { label: string }).label
         ),
       ];
     }
@@ -125,11 +122,13 @@ export const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> =
   );
 };
 
-export const StepRuleDescription = memo(StepRuleDescriptionComponent);
+export const StepRuleDescription = memo(
+  StepRuleDescriptionComponent
+) as typeof StepRuleDescriptionComponent;
 
-export const buildListItems = (
+export const buildListItems = <T,>(
   data: unknown,
-  schema: FormSchema,
+  schema: FormSchema<T>,
   filterManager: FilterManager,
   indexPatterns?: IIndexPattern
 ): ListItems[] =>
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx
index 3152fef12c65..ec46610286b4 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx
@@ -14,7 +14,7 @@ jest.mock('../../../../common/lib/kibana');
 
 describe('MlJobDescription', () => {
   it('renders correctly', () => {
-    const wrapper = shallow(<MlJobDescription job={mockOpenedJob} />);
+    const wrapper = shallow(<MlJobDescription jobId={'myJobId'} />);
 
     expect(wrapper.find('[data-test-subj="machineLearningJobId"]')).toHaveLength(1);
   });
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx
index 6baa2abab33d..414f6f2c2d3b 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx
@@ -10,6 +10,7 @@ import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui';
 
 import { MlSummaryJob } from '../../../../../../ml/public';
 import { isJobStarted } from '../../../../../common/machine_learning/helpers';
+import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs';
 import { useKibana } from '../../../../common/lib/kibana';
 import { ListItems } from './types';
 import { ML_JOB_STARTED, ML_JOB_STOPPED } from './translations';
@@ -69,35 +70,33 @@ const Wrapper = styled.div`
   overflow: hidden;
 `;
 
-const MlJobDescriptionComponent: React.FC<{ job: MlSummaryJob }> = ({ job }) => {
+const MlJobDescriptionComponent: React.FC<{ jobId: string }> = ({ jobId }) => {
+  const { jobs } = useSecurityJobs(false);
   const jobUrl = useKibana().services.application.getUrlForApp(
-    `ml#/jobs?mlManagement=(jobId:${encodeURI(job.id)})`
+    `ml#/jobs?mlManagement=(jobId:${encodeURI(jobId)})`
   );
+  const job = jobs.find(({ id }) => id === jobId);
 
-  return (
+  const jobIdSpan = <span data-test-subj="machineLearningJobId">{jobId}</span>;
+
+  return job != null ? (
     <Wrapper>
       <div>
-        <JobLink data-test-subj="machineLearningJobId" href={jobUrl} target="_blank">
-          {job.id}
+        <JobLink href={jobUrl} target="_blank">
+          {jobIdSpan}
         </JobLink>
         <AuditIcon message={job.auditMessage} />
       </div>
       <JobStatusBadge job={job} />
     </Wrapper>
+  ) : (
+    jobIdSpan
   );
 };
 
 export const MlJobDescription = React.memo(MlJobDescriptionComponent);
 
-export const buildMlJobDescription = (
-  jobId: string,
-  label: string,
-  jobs: MlSummaryJob[]
-): ListItems => {
-  const job = jobs.find(({ id }) => id === jobId);
-
-  return {
-    title: label,
-    description: job ? <MlJobDescription job={job} /> : jobId,
-  };
-};
+export const buildMlJobDescription = (jobId: string, label: string): ListItems => ({
+  title: label,
+  description: <MlJobDescription jobId={jobId} />,
+});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx
index d97c2b4c8c0a..7c8f5230cc8b 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/next_step/index.tsx
@@ -9,7 +9,7 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elasti
 import * as RuleI18n from '../../../pages/detection_engine/rules/translations';
 
 interface NextStepProps {
-  onClick: () => Promise<void>;
+  onClick: () => void;
   isDisabled: boolean;
   dataTestSubj?: string;
 }
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx
index 1ef3edf8c720..08cea23d89e1 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx
@@ -8,12 +8,12 @@ import styled from 'styled-components';
 import { EuiHealth } from '@elastic/eui';
 import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
 import React from 'react';
-import * as I18n from './translations';
 
-export type SeverityValue = 'low' | 'medium' | 'high' | 'critical';
+import { Severity } from '../../../../../common/detection_engine/schemas/common/schemas';
+import * as I18n from './translations';
 
 export interface SeverityOptionItem {
-  value: SeverityValue;
+  value: Severity;
   inputDisplay: React.ReactElement;
 }
 
@@ -44,7 +44,7 @@ export const severityOptions: SeverityOptionItem[] = [
   },
 ];
 
-export const defaultRiskScoreBySeverity: Record<SeverityValue, number> = {
+export const defaultRiskScoreBySeverity: Record<Severity, number> = {
   low: 21,
   medium: 47,
   high: 73,
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts
index b9c3e4f84c18..56c61c2ad676 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts
@@ -21,9 +21,8 @@ export const stepAboutDefaultValue: AboutStepRule = {
   description: '',
   isAssociatedToEndpointList: false,
   isBuildingBlock: false,
-  isNew: true,
   severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
-  riskScore: { value: 50, mapping: [], isMappingChecked: false },
+  riskScore: { value: 21, mapping: [], isMappingChecked: false },
   references: [''],
   falsePositives: [''],
   license: '',
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
index 0c834b9fff33..cb25785eaa5b 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
@@ -4,7 +4,6 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 import React from 'react';
-import { act } from 'react-dom/test-utils';
 import { mount, shallow } from 'enzyme';
 import { ThemeProvider } from 'styled-components';
 import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json';
@@ -14,9 +13,11 @@ import { StepAboutRule } from '.';
 import { mockAboutStepRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock';
 import { StepRuleDescription } from '../description_step';
 import { stepAboutDefaultValue } from './default_value';
-// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
-import { wait as waitFor } from '@testing-library/react';
-import { AboutStepRule } from '../../../pages/detection_engine/rules/types';
+import {
+  AboutStepRule,
+  RuleStepsFormHooks,
+  RuleStep,
+} from '../../../pages/detection_engine/rules/types';
 import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers';
 
 const theme = () => ({ eui: euiDarkVars, darkMode: true });
@@ -33,6 +34,18 @@ afterAll(() => {
 /* eslint-enable no-console */
 
 describe('StepAboutRuleComponent', () => {
+  let formHook: RuleStepsFormHooks[RuleStep.aboutRule] | null = null;
+  const setFormHook = <K extends keyof RuleStepsFormHooks>(
+    step: K,
+    hook: RuleStepsFormHooks[K]
+  ) => {
+    formHook = hook as typeof formHook;
+  };
+
+  beforeEach(() => {
+    formHook = null;
+  });
+
   test('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => {
     const wrapper = shallow(
       <StepAboutRule
@@ -47,7 +60,7 @@ describe('StepAboutRuleComponent', () => {
     expect(wrapper.find(StepRuleDescription).exists()).toBeTruthy();
   });
 
-  test('it prevents user from clicking continue if no "description" defined', () => {
+  it('is invalid if description is not present', async () => {
     const wrapper = mount(
       <ThemeProvider theme={theme}>
         <StepAboutRule
@@ -55,43 +68,26 @@ describe('StepAboutRuleComponent', () => {
           defaultValues={stepAboutDefaultValue}
           descriptionColumns="multi"
           isReadOnlyView={false}
+          setForm={setFormHook}
           isLoading={false}
-          setForm={jest.fn()}
-          setStepData={jest.fn()}
         />
       </ThemeProvider>
     );
 
+    if (!formHook) {
+      throw new Error('Form hook not set, but tests depend on it');
+    }
+
     wrapper
       .find('[data-test-subj="detectionEngineStepAboutRuleName"] input')
       .first()
       .simulate('change', { target: { value: 'Test name text' } });
 
-    const descriptionInput = wrapper
-      .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea')
-      .first();
-    wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click');
-
-    expect(
-      wrapper.find('[data-test-subj="detectionEngineStepAboutRuleName"] input').first().props()
-        .value
-    ).toEqual('Test name text');
-    expect(descriptionInput.props().value).toEqual('');
-    expect(
-      wrapper
-        .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] label')
-        .first()
-        .hasClass('euiFormLabel-isInvalid')
-    ).toBeTruthy();
-    expect(
-      wrapper
-        .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] EuiTextArea')
-        .first()
-        .prop('isInvalid')
-    ).toBeTruthy();
+    const result = await formHook();
+    expect(result?.isValid).toEqual(false);
   });
 
-  test('it prevents user from clicking continue if no "name" defined', () => {
+  it('is invalid if no "name" is present', async () => {
     const wrapper = mount(
       <ThemeProvider theme={theme}>
         <StepAboutRule
@@ -99,47 +95,26 @@ describe('StepAboutRuleComponent', () => {
           defaultValues={stepAboutDefaultValue}
           descriptionColumns="multi"
           isReadOnlyView={false}
+          setForm={setFormHook}
           isLoading={false}
-          setForm={jest.fn()}
-          setStepData={jest.fn()}
         />
       </ThemeProvider>
     );
 
+    if (!formHook) {
+      throw new Error('Form hook not set, but tests depend on it');
+    }
+
     wrapper
       .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea')
       .first()
       .simulate('change', { target: { value: 'Test description text' } });
 
-    const nameInput = wrapper
-      .find('[data-test-subj="detectionEngineStepAboutRuleName"] input')
-      .first();
-
-    wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click');
-
-    expect(
-      wrapper
-        .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea')
-        .first()
-        .props().value
-    ).toEqual('Test description text');
-    expect(nameInput.props().value).toEqual('');
-    expect(
-      wrapper
-        .find('[data-test-subj="detectionEngineStepAboutRuleName"] label')
-        .first()
-        .hasClass('euiFormLabel-isInvalid')
-    ).toBeTruthy();
-    expect(
-      wrapper
-        .find('[data-test-subj="detectionEngineStepAboutRuleName"] EuiFieldText')
-        .first()
-        .prop('isInvalid')
-    ).toBeTruthy();
+    const result = await formHook();
+    expect(result?.isValid).toEqual(false);
   });
 
-  test('it allows user to click continue if "name" and "description" are defined', async () => {
-    const stepDataMock = jest.fn();
+  it('is valid if both "name" and "description" are present', async () => {
     const wrapper = mount(
       <ThemeProvider theme={theme}>
         <StepAboutRule
@@ -147,54 +122,55 @@ describe('StepAboutRuleComponent', () => {
           defaultValues={stepAboutDefaultValue}
           descriptionColumns="multi"
           isReadOnlyView={false}
+          setForm={setFormHook}
           isLoading={false}
-          setForm={jest.fn()}
-          setStepData={stepDataMock}
         />
       </ThemeProvider>
     );
 
+    if (!formHook) {
+      throw new Error('Form hook not set, but tests depend on it');
+    }
+
     wrapper
       .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea')
       .first()
       .simulate('change', { target: { value: 'Test description text' } });
-
     wrapper
       .find('[data-test-subj="detectionEngineStepAboutRuleName"] input')
       .first()
       .simulate('change', { target: { value: 'Test name text' } });
 
-    wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click').update();
-    await waitFor(() => {
-      const expected: Omit<AboutStepRule, 'isNew'> = {
-        author: [],
-        isAssociatedToEndpointList: false,
-        isBuildingBlock: false,
-        license: '',
-        ruleNameOverride: '',
-        timestampOverride: '',
-        description: 'Test description text',
-        falsePositives: [''],
-        name: 'Test name text',
-        note: '',
-        references: [''],
-        riskScore: { value: 50, mapping: [], isMappingChecked: false },
-        severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
-        tags: [],
-        threat: [
-          {
-            framework: 'MITRE ATT&CK',
-            tactic: { id: 'none', name: 'none', reference: 'none' },
-            technique: [],
-          },
-        ],
-      };
-      expect(stepDataMock.mock.calls[1][1]).toEqual(expected);
-    });
+    const expected: AboutStepRule = {
+      author: [],
+      isAssociatedToEndpointList: false,
+      isBuildingBlock: false,
+      license: '',
+      ruleNameOverride: '',
+      timestampOverride: '',
+      description: 'Test description text',
+      falsePositives: [''],
+      name: 'Test name text',
+      note: '',
+      references: [''],
+      riskScore: { value: 21, mapping: [], isMappingChecked: false },
+      severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
+      tags: [],
+      threat: [
+        {
+          framework: 'MITRE ATT&CK',
+          tactic: { id: 'none', name: 'none', reference: 'none' },
+          technique: [],
+        },
+      ],
+    };
+
+    const result = await formHook();
+    expect(result?.isValid).toEqual(true);
+    expect(result?.data).toEqual(expected);
   });
 
   test('it allows user to set the risk score as a number (and not a string)', async () => {
-    const stepDataMock = jest.fn();
     const wrapper = mount(
       <ThemeProvider theme={theme}>
         <StepAboutRule
@@ -202,13 +178,16 @@ describe('StepAboutRuleComponent', () => {
           defaultValues={stepAboutDefaultValue}
           descriptionColumns="multi"
           isReadOnlyView={false}
+          setForm={setFormHook}
           isLoading={false}
-          setForm={jest.fn()}
-          setStepData={stepDataMock}
         />
       </ThemeProvider>
     );
 
+    if (!formHook) {
+      throw new Error('Form hook not set, but tests depend on it');
+    }
+
     wrapper
       .find('[data-test-subj="detectionEngineStepAboutRuleName"] input')
       .first()
@@ -224,11 +203,7 @@ describe('StepAboutRuleComponent', () => {
       .first()
       .simulate('change', { target: { value: '80' } });
 
-    await act(async () => {
-      wrapper.find('[data-test-subj="about-continue"]').first().simulate('click').update();
-    });
-
-    const expected: Omit<AboutStepRule, 'isNew'> = {
+    const expected: AboutStepRule = {
       author: [],
       isAssociatedToEndpointList: false,
       isBuildingBlock: false,
@@ -251,6 +226,52 @@ describe('StepAboutRuleComponent', () => {
         },
       ],
     };
-    expect(stepDataMock.mock.calls[1][1]).toEqual(expected);
+
+    const result = await formHook();
+    expect(result?.isValid).toEqual(true);
+    expect(result?.data).toEqual(expected);
+  });
+
+  it('does not modify the provided risk score until the user changes the severity', async () => {
+    const wrapper = mount(
+      <ThemeProvider theme={theme}>
+        <StepAboutRule
+          addPadding={true}
+          defaultValues={stepAboutDefaultValue}
+          descriptionColumns="multi"
+          isReadOnlyView={false}
+          setForm={setFormHook}
+          isLoading={false}
+        />
+      </ThemeProvider>
+    );
+
+    if (!formHook) {
+      throw new Error('Form hook not set, but tests depend on it');
+    }
+
+    wrapper
+      .find('[data-test-subj="detectionEngineStepAboutRuleName"] input')
+      .first()
+      .simulate('change', { target: { value: 'Test name text' } });
+
+    wrapper
+      .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea')
+      .first()
+      .simulate('change', { target: { value: 'Test description text' } });
+
+    const result = await formHook();
+    expect(result?.isValid).toEqual(true);
+    expect(result?.data?.riskScore.value).toEqual(21);
+
+    wrapper
+      .find('[data-test-subj="detectionEngineStepAboutRuleSeverity"] [data-test-subj="select"]')
+      .last()
+      .simulate('click');
+    wrapper.find('button#medium').simulate('click');
+
+    const result2 = await formHook();
+    expect(result2?.isValid).toEqual(true);
+    expect(result2?.data?.riskScore.value).toEqual(47);
   });
 });
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
index 295b13717f07..d2c84883fa99 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
@@ -22,13 +22,14 @@ import { AddMitreThreat } from '../mitre';
 import {
   Field,
   Form,
-  FormDataProvider,
   getUseField,
   UseField,
   useForm,
+  useFormData,
+  FieldHook,
 } from '../../../../shared_imports';
 
-import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data';
+import { defaultRiskScoreBySeverity, severityOptions } from './data';
 import { stepAboutDefaultValue } from './default_value';
 import { isUrlInvalid } from '../../../../common/utils/validators';
 import { schema } from './schema';
@@ -68,47 +69,69 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
   isReadOnlyView,
   isUpdateView = false,
   isLoading,
+  onSubmit,
   setForm,
-  setStepData,
 }) => {
   const initialState = defaultValues ?? stepAboutDefaultValue;
-  const [myStepData, setMyStepData] = useState<AboutStepRule>(initialState);
+  const [severityValue, setSeverityValue] = useState<string>(initialState.severity.value);
   const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns(
     defineRuleData?.index ?? [],
-    'step_about_rule'
+    RuleStep.aboutRule
   );
   const canUseExceptions =
     defineRuleData?.ruleType &&
     !isMlRule(defineRuleData.ruleType) &&
     !isThresholdRule(defineRuleData.ruleType);
 
-  const { form } = useForm({
+  const { form } = useForm<AboutStepRule>({
     defaultValue: initialState,
     options: { stripEmptyFields: false },
     schema,
   });
-  const { getFields, submit } = form;
+  const { getFields, getFormData, submit } = form;
+  const [{ severity: formSeverity }] = (useFormData({
+    form,
+    watch: ['severity'],
+  }) as unknown) as [Partial<AboutStepRule>];
 
-  const onSubmit = useCallback(async () => {
-    if (setStepData) {
-      setStepData(RuleStep.aboutRule, null, false);
-      const { isValid, data } = await submit();
-      if (isValid) {
-        setStepData(RuleStep.aboutRule, data, isValid);
-        setMyStepData({ ...data, isNew: false } as AboutStepRule);
+  useEffect(() => {
+    const formSeverityValue = formSeverity?.value;
+    if (formSeverityValue != null && formSeverityValue !== severityValue) {
+      setSeverityValue(formSeverityValue);
+
+      const newRiskScoreValue = defaultRiskScoreBySeverity[formSeverityValue];
+      if (newRiskScoreValue != null) {
+        const riskScoreField = getFields().riskScore as FieldHook<AboutStepRule['riskScore']>;
+        riskScoreField.setValue({ ...riskScoreField.value, value: newRiskScoreValue });
       }
     }
-  }, [setStepData, submit]);
+  }, [formSeverity?.value, getFields, severityValue]);
+
+  const getData = useCallback(async () => {
+    const result = await submit();
+    return result?.isValid
+      ? result
+      : {
+          isValid: false,
+          data: getFormData(),
+        };
+  }, [getFormData, submit]);
+
+  const handleSubmit = useCallback(() => {
+    if (onSubmit) {
+      onSubmit();
+    }
+  }, [onSubmit]);
 
   useEffect(() => {
     if (setForm) {
-      setForm(RuleStep.aboutRule, form);
+      setForm(RuleStep.aboutRule, getData);
     }
-  }, [setForm, form]);
+  }, [getData, setForm]);
 
-  return isReadOnlyView && myStepData.name != null ? (
+  return isReadOnlyView ? (
     <StepContentWrapper data-test-subj="aboutStep" addPadding={addPadding}>
-      <StepRuleDescription columns={descriptionColumns} schema={schema} data={myStepData} />
+      <StepRuleDescription columns={descriptionColumns} schema={schema} data={initialState} />
     </StepContentWrapper>
   ) : (
     <>
@@ -305,26 +328,11 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
               }}
             />
           </EuiAccordion>
-          <FormDataProvider pathsToWatch="severity">
-            {({ severity }) => {
-              const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue];
-              const severityField = getFields().severity;
-              const riskScoreField = getFields().riskScore;
-              if (
-                severityField.value !== severity &&
-                newRiskScore != null &&
-                riskScoreField.value !== newRiskScore
-              ) {
-                riskScoreField.setValue(newRiskScore);
-              }
-              return null;
-            }}
-          </FormDataProvider>
         </Form>
       </StepContentWrapper>
 
       {!isUpdateView && (
-        <NextStep dataTestSubj="about-continue" onClick={onSubmit} isDisabled={isLoading} />
+        <NextStep dataTestSubj="about-continue" onClick={handleSubmit} isDisabled={isLoading} />
       )}
     </>
   );
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx
index 2264a11341eb..6df94eca429c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx
@@ -13,7 +13,7 @@ import {
   ValidationFunc,
   ERROR_CODE,
 } from '../../../../shared_imports';
-import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types';
+import { IMitreEnterpriseAttack, AboutStepRule } from '../../../pages/detection_engine/rules/types';
 import { isMitreAttackInvalid } from '../mitre/helpers';
 import { OptionalFieldLabel } from '../optional_field_label';
 import { isUrlInvalid } from '../../../../common/utils/validators';
@@ -21,7 +21,7 @@ import * as I18n from './translations';
 
 const { emptyField } = fieldValidators;
 
-export const schema: FormSchema = {
+export const schema: FormSchema<AboutStepRule> = {
   author: {
     type: FIELD_TYPES.COMBO_BOX,
     label: i18n.translate(
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
index 8a1f96188351..158f323b739e 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
@@ -7,15 +7,14 @@
 import { EuiButtonEmpty, EuiFormRow } from '@elastic/eui';
 import React, { FC, memo, useCallback, useState, useEffect } from 'react';
 import styled from 'styled-components';
-import deepEqual from 'fast-deep-equal';
+import isEqual from 'lodash/isEqual';
 
 import { DEFAULT_INDEX_KEY } from '../../../../../common/constants';
+import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
 import { isMlRule } from '../../../../../common/machine_learning/helpers';
 import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions';
 import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license';
-import { IIndexPattern } from '../../../../../../../../src/plugins/data/public';
 import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules';
-import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
 import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
 import { useUiSetting$ } from '../../../../common/lib/kibana';
 import {
@@ -42,9 +41,8 @@ import {
   getUseField,
   UseField,
   UseMultiFields,
-  FormDataProvider,
   useForm,
-  FormSchema,
+  useFormData,
 } from '../../../../shared_imports';
 import { schema } from './schema';
 import * as i18n from './translations';
@@ -52,13 +50,12 @@ import * as i18n from './translations';
 const CommonUseField = getUseField({ component: Field });
 
 interface StepDefineRuleProps extends RuleStepProps {
-  defaultValues?: DefineStepRule | null;
+  defaultValues?: DefineStepRule;
 }
 
 const stepDefineDefaultValue: DefineStepRule = {
   anomalyThreshold: 50,
   index: [],
-  isNew: true,
   machineLearningJobId: '',
   ruleType: 'query',
   queryBar: {
@@ -103,8 +100,8 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
   isReadOnlyView,
   isLoading,
   isUpdateView = false,
+  onSubmit,
   setForm,
-  setStepData,
 }) => {
   const mlCapabilities = useMlCapabilities();
   const [openTimelineSearch, setOpenTimelineSearch] = useState(false);
@@ -112,38 +109,54 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
   const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
   const initialState = defaultValues ?? {
     ...stepDefineDefaultValue,
-    index: indicesConfig ?? [],
+    index: indicesConfig,
   };
-  const [localRuleType, setLocalRuleType] = useState(initialState.ruleType);
-  const [myStepData, setMyStepData] = useState<DefineStepRule>(initialState);
-  const [
-    { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar },
-  ] = useFetchIndexPatterns(myStepData.index, 'step_define_rule');
-
-  const { form } = useForm({
+  const { form } = useForm<DefineStepRule>({
     defaultValue: initialState,
     options: { stripEmptyFields: false },
     schema,
   });
-  const { getFields, reset, submit } = form;
-  const clearErrors = useCallback(() => reset({ resetValues: false }), [reset]);
+  const { getFields, getFormData, reset, submit } = form;
+  const [{ index: formIndex, ruleType: formRuleType }] = (useFormData({
+    form,
+    watch: ['index', 'ruleType'],
+  }) as unknown) as [Partial<DefineStepRule>];
+  const index = formIndex || initialState.index;
+  const ruleType = formRuleType || initialState.ruleType;
+  const [
+    { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar },
+  ] = useFetchIndexPatterns(index, RuleStep.defineRule);
 
-  const onSubmit = useCallback(async () => {
-    if (setStepData) {
-      setStepData(RuleStep.defineRule, null, false);
-      const { isValid, data } = await submit();
-      if (isValid && setStepData) {
-        setStepData(RuleStep.defineRule, data, isValid);
-        setMyStepData({ ...data, isNew: false } as DefineStepRule);
-      }
+  // reset form when rule type changes
+  useEffect(() => {
+    reset({ resetValues: false });
+  }, [reset, ruleType]);
+
+  useEffect(() => {
+    setIndexModified(!isEqual(index, indicesConfig));
+  }, [index, indicesConfig]);
+
+  const handleSubmit = useCallback(() => {
+    if (onSubmit) {
+      onSubmit();
     }
-  }, [setStepData, submit]);
+  }, [onSubmit]);
+
+  const getData = useCallback(async () => {
+    const result = await submit();
+    return result?.isValid
+      ? result
+      : {
+          isValid: false,
+          data: getFormData(),
+        };
+  }, [getFormData, submit]);
 
   useEffect(() => {
     if (setForm) {
-      setForm(RuleStep.defineRule, form);
+      setForm(RuleStep.defineRule, getData);
     }
-  }, [form, setForm]);
+  }, [getData, setForm]);
 
   const handleResetIndices = useCallback(() => {
     const indexField = getFields().index;
@@ -173,9 +186,9 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
     <StepContentWrapper data-test-subj="definitionRule" addPadding={addPadding}>
       <StepRuleDescription
         columns={descriptionColumns}
-        indexPatterns={indexPatternQueryBar as IIndexPattern}
-        schema={filterRuleFieldsForType(schema as FormSchema & RuleFields, myStepData.ruleType)}
-        data={filterRuleFieldsForType(myStepData, myStepData.ruleType)}
+        indexPatterns={indexPatternQueryBar}
+        schema={filterRuleFieldsForType(schema as typeof schema & RuleFields, ruleType)}
+        data={filterRuleFieldsForType(initialState, ruleType)}
       />
     </StepContentWrapper>
   ) : (
@@ -192,7 +205,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
               isMlAdmin: hasMlAdminPermissions(mlCapabilities),
             }}
           />
-          <RuleTypeEuiFormRow $isVisible={!isMlRule(localRuleType)} fullWidth>
+          <RuleTypeEuiFormRow $isVisible={!isMlRule(ruleType)} fullWidth>
             <>
               <CommonUseField
                 path="index"
@@ -241,7 +254,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
               />
             </>
           </RuleTypeEuiFormRow>
-          <RuleTypeEuiFormRow $isVisible={isMlRule(localRuleType)} fullWidth>
+          <RuleTypeEuiFormRow $isVisible={isMlRule(ruleType)} fullWidth>
             <>
               <UseField
                 path="machineLearningJobId"
@@ -260,7 +273,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
             </>
           </RuleTypeEuiFormRow>
           <RuleTypeEuiFormRow
-            $isVisible={localRuleType === 'threshold'}
+            $isVisible={ruleType === 'threshold'}
             data-test-subj="thresholdInput"
             fullWidth
           >
@@ -269,11 +282,9 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
                 fields={{
                   thresholdField: {
                     path: 'threshold.field',
-                    defaultValue: initialState.threshold.field,
                   },
                   thresholdValue: {
                     path: 'threshold.value',
-                    defaultValue: initialState.threshold.value,
                   },
                 }}
               >
@@ -290,31 +301,11 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
               dataTestSubj: 'detectionEngineStepDefineRuleTimeline',
             }}
           />
-          <FormDataProvider pathsToWatch={['index', 'ruleType']}>
-            {({ index, ruleType }) => {
-              if (index != null) {
-                if (deepEqual(index, indicesConfig) && indexModified) {
-                  setIndexModified(false);
-                } else if (!deepEqual(index, indicesConfig) && !indexModified) {
-                  setIndexModified(true);
-                }
-                if (myStepData.index !== index) {
-                  setMyStepData((prevValue) => ({ ...prevValue, index }));
-                }
-              }
-
-              if (ruleType !== localRuleType) {
-                setLocalRuleType(ruleType);
-                clearErrors();
-              }
-              return null;
-            }}
-          </FormDataProvider>
         </Form>
       </StepContentWrapper>
 
       {!isUpdateView && (
-        <NextStep dataTestSubj="define-continue" onClick={onSubmit} isDisabled={isLoading} />
+        <NextStep dataTestSubj="define-continue" onClick={handleSubmit} isDisabled={isLoading} />
       )}
     </>
   );
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
index 333b28bf27bb..07eff94bbbef 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
@@ -19,9 +19,10 @@ import {
   FormSchema,
   ValidationFunc,
 } from '../../../../shared_imports';
+import { DefineStepRule } from '../../../pages/detection_engine/rules/types';
 import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations';
 
-export const schema: FormSchema = {
+export const schema: FormSchema<DefineStepRule> = {
   index: {
     type: FIELD_TYPES.COMBO_BOX,
     label: i18n.translate(
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx
index 5b4f7677dbc3..e6f1c25bf9da 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx
@@ -13,7 +13,7 @@ import {
   EuiSpacer,
 } from '@elastic/eui';
 import { findIndex } from 'lodash/fp';
-import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
+import React, { FC, memo, useCallback, useEffect, useMemo } from 'react';
 
 import { ActionVariable } from '../../../../../../triggers_actions_ui/public';
 import {
@@ -22,7 +22,7 @@ import {
   ActionsStepRule,
 } from '../../../pages/detection_engine/rules/types';
 import { StepRuleDescription } from '../description_step';
-import { Form, UseField, useForm } from '../../../../shared_imports';
+import { Form, UseField, useForm, useFormData } from '../../../../shared_imports';
 import { StepContentWrapper } from '../step_content_wrapper';
 import {
   ThrottleSelectField,
@@ -40,9 +40,8 @@ interface StepRuleActionsProps extends RuleStepProps {
   actionMessageParams: ActionVariable[];
 }
 
-const stepActionsDefaultValue = {
+const stepActionsDefaultValue: ActionsStepRule = {
   enabled: true,
-  isNew: true,
   actions: [],
   kibanaSiemAppUrl: '',
   throttle: DEFAULT_THROTTLE_OPTION.value,
@@ -65,27 +64,16 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
   isReadOnlyView,
   isLoading,
   isUpdateView = false,
-  setStepData,
+  onSubmit,
   setForm,
   actionMessageParams,
 }) => {
-  const initialState = defaultValues ?? stepActionsDefaultValue;
-  const [myStepData, setMyStepData] = useState<ActionsStepRule>(initialState);
   const {
     services: {
       application,
       triggers_actions_ui: { actionTypeRegistry },
     },
   } = useKibana();
-  const schema = useMemo(() => getSchema({ actionTypeRegistry }), [actionTypeRegistry]);
-
-  const { form } = useForm({
-    defaultValue: initialState,
-    options: { stripEmptyFields: false },
-    schema,
-  });
-  const { submit } = form;
-
   const kibanaAbsoluteUrl = useMemo(
     () =>
       application.getUrlForApp(`${APP_ID}`, {
@@ -93,37 +81,52 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
       }),
     [application]
   );
-
-  const onSubmit = useCallback(
-    async (enabled: boolean) => {
-      if (setStepData) {
-        setStepData(RuleStep.ruleActions, null, false);
-        const { isValid: newIsValid, data } = await submit();
-        if (newIsValid) {
-          setStepData(RuleStep.ruleActions, { ...data, enabled }, newIsValid);
-          setMyStepData({ ...data, isNew: false } as ActionsStepRule);
-        }
+  const initialState = {
+    ...(defaultValues ?? stepActionsDefaultValue),
+    kibanaSiemAppUrl: kibanaAbsoluteUrl,
+  };
+  const schema = useMemo(() => getSchema({ actionTypeRegistry }), [actionTypeRegistry]);
+  const { form } = useForm<ActionsStepRule>({
+    defaultValue: initialState,
+    options: { stripEmptyFields: false },
+    schema,
+  });
+  const { getFields, getFormData, submit } = form;
+  const [{ throttle: formThrottle }] = (useFormData({
+    form,
+    watch: ['throttle'],
+  }) as unknown) as [Partial<ActionsStepRule>];
+  const throttle = formThrottle || initialState.throttle;
+
+  const handleSubmit = useCallback(
+    (enabled: boolean) => {
+      getFields().enabled.setValue(enabled);
+      if (onSubmit) {
+        onSubmit();
       }
     },
-    [setStepData, submit]
+    [getFields, onSubmit]
   );
 
+  const getData = useCallback(async () => {
+    const result = await submit();
+    return result?.isValid
+      ? result
+      : {
+          isValid: false,
+          data: getFormData(),
+        };
+  }, [getFormData, submit]);
+
   useEffect(() => {
     if (setForm) {
-      setForm(RuleStep.ruleActions, form);
+      setForm(RuleStep.ruleActions, getData);
     }
-  }, [form, setForm]);
-
-  const updateThrottle = useCallback((throttle) => setMyStepData({ ...myStepData, throttle }), [
-    myStepData,
-    setMyStepData,
-  ]);
+  }, [getData, setForm]);
 
   const throttleOptions = useMemo(() => {
-    const throttle = myStepData.throttle;
-
     return getThrottleOptions(throttle);
-  }, [myStepData]);
+  }, [throttle]);
 
   const throttleFieldComponentProps = useMemo(
     () => ({
@@ -131,18 +134,16 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
       isDisabled: isLoading,
       dataTestSubj: 'detectionEngineStepRuleActionsThrottle',
       hasNoInitialSelection: false,
-      handleChange: updateThrottle,
       euiFieldProps: {
         options: throttleOptions,
       },
     }),
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [isLoading, updateThrottle]
+    [isLoading, throttleOptions]
   );
 
-  return isReadOnlyView && myStepData != null ? (
+  return isReadOnlyView ? (
     <StepContentWrapper addPadding={addPadding}>
-      <StepRuleDescription schema={schema} data={myStepData} columns="single" />
+      <StepRuleDescription schema={schema} data={initialState} columns="single" />
     </StepContentWrapper>
   ) : (
     <>
@@ -154,12 +155,11 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
               component={ThrottleSelectField}
               componentProps={throttleFieldComponentProps}
             />
-            {myStepData.throttle !== stepActionsDefaultValue.throttle ? (
+            {throttle !== stepActionsDefaultValue.throttle ? (
               <>
                 <EuiSpacer />
                 <UseField
                   path="actions"
-                  defaultValue={myStepData.actions}
                   component={RuleActionsField}
                   componentProps={{
                     messageVariables: actionMessageParams,
@@ -167,18 +167,10 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
                 />
               </>
             ) : (
-              <UseField
-                path="actions"
-                defaultValue={myStepData.actions}
-                component={GhostFormField}
-              />
+              <UseField path="actions" component={GhostFormField} />
             )}
-            <UseField
-              path="kibanaSiemAppUrl"
-              defaultValue={kibanaAbsoluteUrl}
-              component={GhostFormField}
-            />
-            <UseField path="enabled" defaultValue={myStepData.enabled} component={GhostFormField} />
+            <UseField path="kibanaSiemAppUrl" component={GhostFormField} />
+            <UseField path="enabled" component={GhostFormField} />
           </EuiForm>
         </Form>
       </StepContentWrapper>
@@ -197,7 +189,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
                 fill={false}
                 isDisabled={isLoading}
                 isLoading={isLoading}
-                onClick={onSubmit.bind(null, false)}
+                onClick={() => handleSubmit(false)}
               >
                 {I18n.COMPLETE_WITHOUT_ACTIVATING}
               </EuiButton>
@@ -207,7 +199,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
                 fill
                 isDisabled={isLoading}
                 isLoading={isLoading}
-                onClick={onSubmit.bind(null, true)}
+                onClick={() => handleSubmit(true)}
                 data-test-subj="create-activate"
               >
                 {I18n.COMPLETE_WITH_ACTIVATING}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx
index a093f991afaf..38de3a2026ec 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx
@@ -12,7 +12,8 @@ import {
   AlertAction,
   ActionTypeRegistryContract,
 } from '../../../../../../triggers_actions_ui/public';
-import { FormSchema, FormData, ValidationFunc, ERROR_CODE } from '../../../../shared_imports';
+import { FormSchema, ValidationFunc, ERROR_CODE } from '../../../../shared_imports';
+import { ActionsStepRule } from '../../../pages/detection_engine/rules/types';
 import * as I18n from './translations';
 import { isUuidv4, getActionTypeName, validateMustache, validateActionParams } from './utils';
 
@@ -61,7 +62,7 @@ export const getSchema = ({
   actionTypeRegistry,
 }: {
   actionTypeRegistry: ActionTypeRegistryContract;
-}): FormSchema<FormData> => ({
+}): FormSchema<ActionsStepRule> => ({
   actions: {
     validations: [
       {
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx
index 52f04f8423be..d451932a6b63 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx
@@ -4,7 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import React, { FC, memo, useCallback, useEffect, useState } from 'react';
+import React, { FC, memo, useCallback, useEffect } from 'react';
 
 import {
   RuleStep,
@@ -22,9 +22,8 @@ interface StepScheduleRuleProps extends RuleStepProps {
   defaultValues?: ScheduleStepRule | null;
 }
 
-const stepScheduleDefaultValue = {
+const stepScheduleDefaultValue: ScheduleStepRule = {
   interval: '5m',
-  isNew: true,
   from: '1m',
 };
 
@@ -35,39 +34,44 @@ const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
   isReadOnlyView,
   isLoading,
   isUpdateView = false,
-  setStepData,
+  onSubmit,
   setForm,
 }) => {
   const initialState = defaultValues ?? stepScheduleDefaultValue;
-  const [myStepData, setMyStepData] = useState<ScheduleStepRule>(initialState);
 
-  const { form } = useForm({
+  const { form } = useForm<ScheduleStepRule>({
     defaultValue: initialState,
     options: { stripEmptyFields: false },
     schema,
   });
-  const { submit } = form;
 
-  const onSubmit = useCallback(async () => {
-    if (setStepData) {
-      setStepData(RuleStep.scheduleRule, null, false);
-      const { isValid: newIsValid, data } = await submit();
-      if (newIsValid) {
-        setStepData(RuleStep.scheduleRule, { ...data }, newIsValid);
-        setMyStepData({ ...data, isNew: false } as ScheduleStepRule);
-      }
+  const { getFormData, submit } = form;
+
+  const handleSubmit = useCallback(() => {
+    if (onSubmit) {
+      onSubmit();
     }
-  }, [setStepData, submit]);
+  }, [onSubmit]);
+
+  const getData = useCallback(async () => {
+    const result = await submit();
+    return result?.isValid
+      ? result
+      : {
+          isValid: false,
+          data: getFormData(),
+        };
+  }, [getFormData, submit]);
 
   useEffect(() => {
     if (setForm) {
-      setForm(RuleStep.scheduleRule, form);
+      setForm(RuleStep.scheduleRule, getData);
     }
-  }, [form, setForm]);
+  }, [getData, setForm]);
 
-  return isReadOnlyView && myStepData != null ? (
+  return isReadOnlyView ? (
     <StepContentWrapper addPadding={addPadding}>
-      <StepRuleDescription columns={descriptionColumns} schema={schema} data={myStepData} />
+      <StepRuleDescription columns={descriptionColumns} schema={schema} data={initialState} />
     </StepContentWrapper>
   ) : (
     <>
@@ -96,7 +100,7 @@ const StepScheduleRuleComponent: FC<StepScheduleRuleProps> = ({
       </StepContentWrapper>
 
       {!isUpdateView && (
-        <NextStep dataTestSubj="schedule-continue" onClick={onSubmit} isDisabled={isLoading} />
+        <NextStep dataTestSubj="schedule-continue" onClick={handleSubmit} isDisabled={isLoading} />
       )}
     </>
   );
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx
index f4c371a2364f..cf93a9b61710 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/schema.tsx
@@ -9,9 +9,10 @@
 import { i18n } from '@kbn/i18n';
 
 import { OptionalFieldLabel } from '../optional_field_label';
+import { ScheduleStepRule } from '../../../pages/detection_engine/rules/types';
 import { FormSchema } from '../../../../shared_imports';
 
-export const schema: FormSchema = {
+export const schema: FormSchema<ScheduleStepRule> = {
   interval: {
     label: i18n.translate(
       'xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel',
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts
index 3cd819b55685..19007c4d2e43 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts
@@ -67,7 +67,7 @@ describe('Detections Alerts API', () => {
       });
       expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', {
         body:
-          '{"status":"closed","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}',
+          '{"conflicts":"proceed","status":"closed","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}',
         method: 'POST',
         signal: abortCtrl.signal,
       });
@@ -81,7 +81,7 @@ describe('Detections Alerts API', () => {
       });
       expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', {
         body:
-          '{"status":"open","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}',
+          '{"conflicts":"proceed","status":"open","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}',
         method: 'POST',
         signal: abortCtrl.signal,
       });
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts
index 3fe676fe2c7d..a8a2ae10a3bb 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts
@@ -58,7 +58,7 @@ export const updateAlertStatus = async ({
 }: UpdateAlertStatusProps): Promise<UpdateDocumentByQueryResponse> =>
   KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, {
     method: 'POST',
-    body: JSON.stringify({ status, ...query }),
+    body: JSON.stringify({ conflicts: 'proceed', status, ...query }),
     signal,
   });
 
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts
index f12a5d523bad..0ed091f2c18a 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts
@@ -5,9 +5,9 @@
  */
 
 import {
-  AddRulesProps,
   PatchRuleProps,
-  NewRule,
+  CreateRulesProps,
+  UpdateRulesProps,
   PrePackagedRulesStatusResponse,
   BasicFetchProps,
   RuleStatusResponse,
@@ -16,13 +16,18 @@ import {
   FetchRulesResponse,
   FetchRulesProps,
 } from '../types';
-import { ruleMock, savedRuleMock, rulesMock } from '../mock';
+import { savedRuleMock, rulesMock } from '../mock';
+import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
+import { RulesSchema } from '../../../../../../common/detection_engine/schemas/response';
 
-export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule> =>
-  Promise.resolve(ruleMock);
+export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise<RulesSchema> =>
+  Promise.resolve(getRulesSchemaMock());
 
-export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise<NewRule> =>
-  Promise.resolve(ruleMock);
+export const createRule = async ({ rule, signal }: CreateRulesProps): Promise<RulesSchema> =>
+  Promise.resolve(getRulesSchemaMock());
+
+export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise<RulesSchema> =>
+  Promise.resolve(getRulesSchemaMock());
 
 export const getPrePackagedRulesStatus = async ({
   signal,
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts
index f58c95ed71e2..cd1ded544cfe 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts
@@ -6,7 +6,9 @@
 
 import { KibanaServices } from '../../../../common/lib/kibana';
 import {
-  addRule,
+  createRule,
+  updateRule,
+  patchRule,
   fetchRules,
   fetchRuleById,
   enableRules,
@@ -19,9 +21,12 @@ import {
   fetchTags,
   getPrePackagedRulesStatus,
 } from './api';
-import { ruleMock, rulesMock } from './mock';
+import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
+import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock';
+import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock';
+import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock';
+import { rulesMock } from './mock';
 import { buildEsQuery } from 'src/plugins/data/common';
-
 const abortCtrl = new AbortController();
 const mockKibanaServices = KibanaServices.get as jest.Mock;
 jest.mock('../../../../common/lib/kibana');
@@ -30,25 +35,56 @@ const fetchMock = jest.fn();
 mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } });
 
 describe('Detections Rules API', () => {
-  describe('addRule', () => {
+  describe('createRule', () => {
     beforeEach(() => {
       fetchMock.mockClear();
-      fetchMock.mockResolvedValue(ruleMock);
+      fetchMock.mockResolvedValue(getRulesSchemaMock());
     });
 
-    test('check parameter url, body', async () => {
-      await addRule({ rule: ruleMock, signal: abortCtrl.signal });
+    test('POSTs rule', async () => {
+      const payload = getCreateRulesSchemaMock();
+      await createRule({ rule: payload, signal: abortCtrl.signal });
       expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', {
         body:
-          '{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[],"throttle":null}',
+          '{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1"}',
         method: 'POST',
         signal: abortCtrl.signal,
       });
     });
+  });
 
-    test('happy path', async () => {
-      const ruleResp = await addRule({ rule: ruleMock, signal: abortCtrl.signal });
-      expect(ruleResp).toEqual(ruleMock);
+  describe('updateRule', () => {
+    beforeEach(() => {
+      fetchMock.mockClear();
+      fetchMock.mockResolvedValue(getRulesSchemaMock());
+    });
+
+    test('PUTs rule', async () => {
+      const payload = getUpdateRulesSchemaMock();
+      await updateRule({ rule: payload, signal: abortCtrl.signal });
+      expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', {
+        body:
+          '{"description":"some description","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1"}',
+        method: 'PUT',
+        signal: abortCtrl.signal,
+      });
+    });
+  });
+
+  describe('patchRule', () => {
+    beforeEach(() => {
+      fetchMock.mockClear();
+      fetchMock.mockResolvedValue(getRulesSchemaMock());
+    });
+
+    test('PATCHs rule', async () => {
+      const payload = getPatchRulesSchemaMock();
+      await patchRule({ ruleProperties: payload, signal: abortCtrl.signal });
+      expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', {
+        body: JSON.stringify(payload),
+        method: 'PATCH',
+        signal: abortCtrl.signal,
+      });
     });
   });
 
@@ -280,7 +316,7 @@ describe('Detections Rules API', () => {
   describe('fetchRuleById', () => {
     beforeEach(() => {
       fetchMock.mockClear();
-      fetchMock.mockResolvedValue(ruleMock);
+      fetchMock.mockResolvedValue(getRulesSchemaMock());
     });
 
     test('check parameter url, query', async () => {
@@ -296,7 +332,7 @@ describe('Detections Rules API', () => {
 
     test('happy path', async () => {
       const ruleResp = await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal });
-      expect(ruleResp).toEqual(ruleMock);
+      expect(ruleResp).toEqual(getRulesSchemaMock());
     });
   });
 
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
index 3538d8ec8c9b..e254516d1107 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
@@ -3,7 +3,6 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-
 import { HttpStart } from '../../../../../../../../src/core/public';
 import {
   DETECTION_ENGINE_RULES_URL,
@@ -13,13 +12,13 @@ import {
   DETECTION_ENGINE_TAGS_URL,
 } from '../../../../../common/constants';
 import {
-  AddRulesProps,
+  UpdateRulesProps,
+  CreateRulesProps,
   DeleteRulesProps,
   DuplicateRulesProps,
   EnableRulesProps,
   FetchRulesProps,
   FetchRulesResponse,
-  NewRule,
   Rule,
   FetchRuleProps,
   BasicFetchProps,
@@ -33,32 +32,51 @@ import {
 } from './types';
 import { KibanaServices } from '../../../../common/lib/kibana';
 import * as i18n from '../../../pages/detection_engine/rules/translations';
+import { RulesSchema } from '../../../../../common/detection_engine/schemas/response';
 
 /**
- * Add provided Rule
+ * Create provided Rule
  *
- * @param rule to add
+ * @param rule CreateRulesSchema to add
  * @param signal to cancel request
  *
  * @throws An error if response is not OK
  */
-export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule> =>
-  KibanaServices.get().http.fetch<NewRule>(DETECTION_ENGINE_RULES_URL, {
-    method: rule.id != null ? 'PUT' : 'POST',
+export const createRule = async ({ rule, signal }: CreateRulesProps): Promise<RulesSchema> =>
+  KibanaServices.get().http.fetch<RulesSchema>(DETECTION_ENGINE_RULES_URL, {
+    method: 'POST',
+    body: JSON.stringify(rule),
+    signal,
+  });
+
+/**
+ * Update provided Rule using PUT
+ *
+ * @param rule UpdateRulesSchema to be updated
+ * @param signal to cancel request
+ *
+ * @throws An error if response is not OK
+ */
+export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise<RulesSchema> =>
+  KibanaServices.get().http.fetch<RulesSchema>(DETECTION_ENGINE_RULES_URL, {
+    method: 'PUT',
     body: JSON.stringify(rule),
     signal,
   });
 
 /**
- * Patch provided Rule
+ * Patch provided rule
+ * NOTE: The rule edit flow does NOT use patch as it relies on the
+ * functionality of PUT to delete field values when not provided, if
+ * just expecting changes, use this `patchRule`
  *
  * @param ruleProperties to patch
  * @param signal to cancel request
  *
  * @throws An error if response is not OK
  */
-export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise<NewRule> =>
-  KibanaServices.get().http.fetch<NewRule>(DETECTION_ENGINE_RULES_URL, {
+export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise<RulesSchema> =>
+  KibanaServices.get().http.fetch<RulesSchema>(DETECTION_ENGINE_RULES_URL, {
     method: 'PATCH',
     body: JSON.stringify(ruleProperties),
     signal,
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts
index c7ecfb33cd90..a40ab2e48785 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts
@@ -6,7 +6,8 @@
 
 export * from './api';
 export * from './fetch_index_patterns';
-export * from './persist_rule';
+export * from './use_update_rule';
+export * from './use_create_rule';
 export * from './types';
 export * from './use_rule';
 export * from './use_rules';
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts
index fa11cfabcdf8..c0397b0af6db 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts
@@ -4,36 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { NewRule, FetchRulesResponse, Rule } from './types';
-
-export const ruleMock: NewRule = {
-  description: 'some desc',
-  enabled: true,
-  false_positives: [],
-  filters: [],
-  from: 'now-360s',
-  index: [
-    'apm-*-transaction*',
-    'auditbeat-*',
-    'endgame-*',
-    'filebeat-*',
-    'packetbeat-*',
-    'winlogbeat-*',
-  ],
-  interval: '5m',
-  rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf',
-  language: 'kuery',
-  risk_score: 75,
-  name: 'Test rule',
-  query: "user.email: 'root@elastic.co'",
-  references: [],
-  severity: 'high',
-  tags: ['APM'],
-  to: 'now',
-  type: 'query',
-  threat: [],
-  throttle: null,
-};
+import { FetchRulesResponse, Rule } from './types';
 
 export const savedRuleMock: Rule = {
   author: [],
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
index 166bb90113ae..e94e57ad82bc 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
@@ -7,6 +7,7 @@
 import * as t from 'io-ts';
 
 import {
+  SortOrder,
   author,
   building_block_type,
   license,
@@ -17,11 +18,12 @@ import {
   threshold,
   type,
 } from '../../../../../common/detection_engine/schemas/common/schemas';
+import { listArray } from '../../../../../common/detection_engine/schemas/types';
 import {
-  listArray,
-  listArrayOrUndefined,
-} from '../../../../../common/detection_engine/schemas/types';
-import { PatchRulesSchema } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema';
+  CreateRulesSchema,
+  PatchRulesSchema,
+  UpdateRulesSchema,
+} from '../../../../../common/detection_engine/schemas/request';
 
 /**
  * Params is an "record", since it is a type of AlertActionParams which is action templates.
@@ -36,48 +38,13 @@ export const action = t.exact(
   })
 );
 
-export const NewRuleSchema = t.intersection([
-  t.type({
-    description: t.string,
-    enabled: t.boolean,
-    interval: t.string,
-    name: t.string,
-    risk_score: t.number,
-    severity: t.string,
-    type,
-  }),
-  t.partial({
-    actions: t.array(action),
-    anomaly_threshold: t.number,
-    created_by: t.string,
-    false_positives: t.array(t.string),
-    filters: t.array(t.unknown),
-    from: t.string,
-    id: t.string,
-    index: t.array(t.string),
-    language: t.string,
-    machine_learning_job_id: t.string,
-    max_signals: t.number,
-    query: t.string,
-    references: t.array(t.string),
-    rule_id: t.string,
-    saved_id: t.string,
-    tags: t.array(t.string),
-    threat: t.array(t.unknown),
-    threshold,
-    throttle: t.union([t.string, t.null]),
-    to: t.string,
-    updated_by: t.string,
-    note: t.string,
-    exceptions_list: listArrayOrUndefined,
-  }),
-]);
-
-export const NewRulesSchema = t.array(NewRuleSchema);
-export type NewRule = t.TypeOf<typeof NewRuleSchema>;
+export interface CreateRulesProps {
+  rule: CreateRulesSchema;
+  signal: AbortSignal;
+}
 
-export interface AddRulesProps {
-  rule: NewRule;
+export interface UpdateRulesProps {
+  rule: UpdateRulesSchema;
   signal: AbortSignal;
 }
 
@@ -185,7 +152,7 @@ export interface FetchRulesProps {
 export interface FilterOptions {
   filter: string;
   sortField: string;
-  sortOrder: 'asc' | 'desc';
+  sortOrder: SortOrder;
   showCustomRules?: boolean;
   showElasticRules?: boolean;
   tags?: string[];
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx
similarity index 68%
rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx
index 1bf21623992e..42d6a2a92a4c 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx
@@ -6,25 +6,25 @@
 
 import { renderHook, act } from '@testing-library/react-hooks';
 
-import { usePersistRule, ReturnPersistRule } from './persist_rule';
-import { ruleMock } from './mock';
+import { useCreateRule, ReturnCreateRule } from './use_create_rule';
+import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock';
 
 jest.mock('./api');
 
-describe('usePersistRule', () => {
+describe('useCreateRule', () => {
   test('init', async () => {
-    const { result } = renderHook<unknown, ReturnPersistRule>(() => usePersistRule());
+    const { result } = renderHook<unknown, ReturnCreateRule>(() => useCreateRule());
 
     expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
   });
 
   test('saving rule with isLoading === true', async () => {
     await act(async () => {
-      const { result, rerender, waitForNextUpdate } = renderHook<void, ReturnPersistRule>(() =>
-        usePersistRule()
+      const { result, rerender, waitForNextUpdate } = renderHook<void, ReturnCreateRule>(() =>
+        useCreateRule()
       );
       await waitForNextUpdate();
-      result.current[1](ruleMock);
+      result.current[1](getUpdateRulesSchemaMock());
       rerender();
       expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
     });
@@ -32,11 +32,11 @@ describe('usePersistRule', () => {
 
   test('saved rule with isSaved === true', async () => {
     await act(async () => {
-      const { result, waitForNextUpdate } = renderHook<void, ReturnPersistRule>(() =>
-        usePersistRule()
+      const { result, waitForNextUpdate } = renderHook<void, ReturnCreateRule>(() =>
+        useCreateRule()
       );
       await waitForNextUpdate();
-      result.current[1](ruleMock);
+      result.current[1](getUpdateRulesSchemaMock());
       await waitForNextUpdate();
       expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
     });
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx
similarity index 76%
rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx
rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx
index fd139d59c0a2..2bbd27994fc7 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx
@@ -7,20 +7,20 @@
 import { useEffect, useState, Dispatch } from 'react';
 
 import { errorToToaster, useStateToaster } from '../../../../common/components/toasters';
+import { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request';
 
-import { addRule as persistRule } from './api';
+import { createRule } from './api';
 import * as i18n from './translations';
-import { NewRule } from './types';
 
-interface PersistRuleReturn {
+interface CreateRuleReturn {
   isLoading: boolean;
   isSaved: boolean;
 }
 
-export type ReturnPersistRule = [PersistRuleReturn, Dispatch<NewRule | null>];
+export type ReturnCreateRule = [CreateRuleReturn, Dispatch<CreateRulesSchema | null>];
 
-export const usePersistRule = (): ReturnPersistRule => {
-  const [rule, setRule] = useState<NewRule | null>(null);
+export const useCreateRule = (): ReturnCreateRule => {
+  const [rule, setRule] = useState<CreateRulesSchema | null>(null);
   const [isSaved, setIsSaved] = useState(false);
   const [isLoading, setIsLoading] = useState(false);
   const [, dispatchToaster] = useStateToaster();
@@ -33,7 +33,7 @@ export const usePersistRule = (): ReturnPersistRule => {
       if (rule != null) {
         try {
           setIsLoading(true);
-          await persistRule({ rule, signal: abortCtrl.signal });
+          await createRule({ rule, signal: abortCtrl.signal });
           if (isSubscribed) {
             setIsSaved(true);
           }
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx
index 6721d89f2799..2ba78cd90cf9 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx
@@ -9,7 +9,7 @@ import { act, renderHook } from '@testing-library/react-hooks';
 import { coreMock } from '../../../../../../../../src/core/public/mocks';
 
 import * as api from './api';
-import { ruleMock } from './mock';
+import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
 import {
   ReturnUseDissasociateExceptionList,
   UseDissasociateExceptionListProps,
@@ -23,7 +23,7 @@ describe('useDissasociateExceptionList', () => {
   const onSuccess = jest.fn();
 
   beforeEach(() => {
-    jest.spyOn(api, 'patchRule').mockResolvedValue(ruleMock);
+    jest.spyOn(api, 'patchRule').mockResolvedValue(getRulesSchemaMock());
   });
 
   afterEach(() => {
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
index 9a6ea4f60fdc..92d46a785b03 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
@@ -10,7 +10,7 @@ import * as api from './api';
 
 jest.mock('./api');
 
-describe('usePersistRule', () => {
+describe('usePrePackagedRules', () => {
   beforeEach(() => {
     jest.clearAllMocks();
     jest.restoreAllMocks();
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx
new file mode 100644
index 000000000000..9603a4151933
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { renderHook, act } from '@testing-library/react-hooks';
+
+import { useUpdateRule, ReturnUpdateRule } from './use_update_rule';
+import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock';
+
+jest.mock('./api');
+
+describe('useUpdateRule', () => {
+  test('init', async () => {
+    const { result } = renderHook<unknown, ReturnUpdateRule>(() => useUpdateRule());
+
+    expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
+  });
+
+  test('saving rule with isLoading === true', async () => {
+    await act(async () => {
+      const { result, rerender, waitForNextUpdate } = renderHook<void, ReturnUpdateRule>(() =>
+        useUpdateRule()
+      );
+      await waitForNextUpdate();
+      result.current[1](getUpdateRulesSchemaMock());
+      rerender();
+      expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
+    });
+  });
+
+  test('saved rule with isSaved === true', async () => {
+    await act(async () => {
+      const { result, waitForNextUpdate } = renderHook<void, ReturnUpdateRule>(() =>
+        useUpdateRule()
+      );
+      await waitForNextUpdate();
+      result.current[1](getUpdateRulesSchemaMock());
+      await waitForNextUpdate();
+      expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
+    });
+  });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx
new file mode 100644
index 000000000000..a437974e93ba
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useEffect, useState, Dispatch } from 'react';
+
+import { errorToToaster, useStateToaster } from '../../../../common/components/toasters';
+import { UpdateRulesSchema } from '../../../../../common/detection_engine/schemas/request';
+
+import { updateRule } from './api';
+import * as i18n from './translations';
+
+interface UpdateRuleReturn {
+  isLoading: boolean;
+  isSaved: boolean;
+}
+
+export type ReturnUpdateRule = [UpdateRuleReturn, Dispatch<UpdateRulesSchema | null>];
+
+export const useUpdateRule = (): ReturnUpdateRule => {
+  const [rule, setRule] = useState<UpdateRulesSchema | null>(null);
+  const [isSaved, setIsSaved] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [, dispatchToaster] = useStateToaster();
+
+  useEffect(() => {
+    let isSubscribed = true;
+    const abortCtrl = new AbortController();
+    setIsSaved(false);
+    async function saveRule() {
+      if (rule != null) {
+        try {
+          setIsLoading(true);
+          await updateRule({ rule, signal: abortCtrl.signal });
+          if (isSubscribed) {
+            setIsSaved(true);
+          }
+        } catch (error) {
+          if (isSubscribed) {
+            errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster });
+          }
+        }
+        if (isSubscribed) {
+          setIsLoading(false);
+        }
+      }
+    }
+
+    saveRule();
+    return () => {
+      isSubscribed = false;
+      abortCtrl.abort();
+    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [rule]);
+
+  return [{ isLoading, isSaved }, setRule];
+};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts
index 8c6e91254314..5a626ce0ff00 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts
@@ -165,8 +165,7 @@ export const mockRuleWithEverything = (id: string): Rule => ({
 });
 
 // TODO: update types mapping
-export const mockAboutStepRule = (isNew = false): AboutStepRule => ({
-  isNew,
+export const mockAboutStepRule = (): AboutStepRule => ({
   author: ['Elastic'],
   isAssociatedToEndpointList: false,
   isBuildingBlock: false,
@@ -200,16 +199,14 @@ export const mockAboutStepRule = (isNew = false): AboutStepRule => ({
   note: '# this is some markdown documentation',
 });
 
-export const mockActionsStepRule = (isNew = false, enabled = false): ActionsStepRule => ({
-  isNew,
+export const mockActionsStepRule = (enabled = false): ActionsStepRule => ({
   actions: [],
   kibanaSiemAppUrl: 'http://localhost:5601/app/siem',
   enabled,
   throttle: 'no_actions',
 });
 
-export const mockDefineStepRule = (isNew = false): DefineStepRule => ({
-  isNew,
+export const mockDefineStepRule = (): DefineStepRule => ({
   ruleType: 'query',
   anomalyThreshold: 50,
   machineLearningJobId: '',
@@ -225,8 +222,7 @@ export const mockDefineStepRule = (isNew = false): DefineStepRule => ({
   },
 });
 
-export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({
-  isNew,
+export const mockScheduleStepRule = (): ScheduleStepRule => ({
   interval: '5m',
   from: '6m',
   to: 'now',
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
index d6dc97fbae15..79488231b29e 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
@@ -5,7 +5,8 @@
  */
 
 import { List } from '../../../../../../common/detection_engine/schemas/types';
-import { NewRule } from '../../../../containers/detection_engine/rules';
+import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request/create_rules_schema';
+import { Rule } from '../../../../containers/detection_engine/rules';
 import {
   getListMock,
   getEndpointListMock,
@@ -721,13 +722,13 @@ describe('helpers', () => {
       mockActions = mockActionsStepRule();
     });
 
-    test('returns NewRule with type of saved_query when saved_id exists', () => {
-      const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions);
+    test('returns rule with type of saved_query when saved_id exists', () => {
+      const result: Rule = formatRule<Rule>(mockDefine, mockAbout, mockSchedule, mockActions);
 
       expect(result.type).toEqual('saved_query');
     });
 
-    test('returns NewRule with type of query when saved_id does not exist', () => {
+    test('returns rule with type of query when saved_id does not exist', () => {
       const mockDefineStepRuleWithoutSavedId = {
         ...mockDefine,
         queryBar: {
@@ -735,7 +736,7 @@ describe('helpers', () => {
           saved_id: '',
         },
       };
-      const result: NewRule = formatRule(
+      const result: CreateRulesSchema = formatRule<CreateRulesSchema>(
         mockDefineStepRuleWithoutSavedId,
         mockAbout,
         mockSchedule,
@@ -745,10 +746,15 @@ describe('helpers', () => {
       expect(result.type).toEqual('query');
     });
 
-    test('returns NewRule without id if ruleId does not exist', () => {
-      const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions);
+    test('returns rule without id if ruleId does not exist', () => {
+      const result: CreateRulesSchema = formatRule<CreateRulesSchema>(
+        mockDefine,
+        mockAbout,
+        mockSchedule,
+        mockActions
+      );
 
-      expect(result.id).toBeUndefined();
+      expect(result).not.toHaveProperty<CreateRulesSchema>('id');
     });
   });
 
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
index f4a40b771c9f..0137777f8f8f 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
@@ -13,8 +13,8 @@ import { transformAlertToRuleAction } from '../../../../../../common/detection_e
 import { isMlRule } from '../../../../../../common/machine_learning/helpers';
 import { isThresholdRule } from '../../../../../../common/detection_engine/utils';
 import { List } from '../../../../../../common/detection_engine/schemas/types';
-import { ENDPOINT_LIST_ID } from '../../../../../shared_imports';
-import { NewRule, Rule } from '../../../../containers/detection_engine/rules';
+import { ENDPOINT_LIST_ID, ExceptionListType, NamespaceType } from '../../../../../shared_imports';
+import { Rule } from '../../../../containers/detection_engine/rules';
 import { Type } from '../../../../../../common/detection_engine/schemas/common/schemas';
 
 import {
@@ -26,6 +26,8 @@ import {
   ScheduleStepRuleJson,
   AboutStepRuleJson,
   ActionsStepRuleJson,
+  RuleStepsFormData,
+  RuleStep,
 } from '../types';
 
 export const getTimeTypeValue = (time: string): { unit: string; value: number } => {
@@ -33,8 +35,8 @@ export const getTimeTypeValue = (time: string): { unit: string; value: number }
     unit: '',
     value: 0,
   };
-  const filterTimeVal = (time as string).match(/\d+/g);
-  const filterTimeType = (time as string).match(/[a-zA-Z]+/g);
+  const filterTimeVal = time.match(/\d+/g);
+  const filterTimeType = time.match(/[a-zA-Z]+/g);
   if (!isEmpty(filterTimeVal) && filterTimeVal != null && !isNaN(Number(filterTimeVal[0]))) {
     timeObj.value = Number(filterTimeVal[0]);
   }
@@ -48,6 +50,23 @@ export const getTimeTypeValue = (time: string): { unit: string; value: number }
   return timeObj;
 };
 
+export const stepIsValid = <T extends RuleStepsFormData[keyof RuleStepsFormData]>(
+  formData?: T
+): formData is { [K in keyof T]: Exclude<T[K], undefined> } =>
+  !!formData?.isValid && !!formData.data;
+
+export const isDefineStep = (input: unknown): input is RuleStepsFormData[RuleStep.defineRule] =>
+  has('data.ruleType', input);
+
+export const isAboutStep = (input: unknown): input is RuleStepsFormData[RuleStep.aboutRule] =>
+  has('data.name', input);
+
+export const isScheduleStep = (input: unknown): input is RuleStepsFormData[RuleStep.scheduleRule] =>
+  has('data.interval', input);
+
+export const isActionsStep = (input: unknown): input is RuleStepsFormData[RuleStep.ruleActions] =>
+  has('data.actions', input);
+
 export interface RuleFields {
   anomalyThreshold: unknown;
   machineLearningJobId: unknown;
@@ -129,7 +148,7 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep
 };
 
 export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => {
-  const { isNew, ...formatScheduleData } = scheduleData;
+  const { ...formatScheduleData } = scheduleData;
   if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) {
     const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue(
       formatScheduleData.interval
@@ -161,7 +180,6 @@ export const formatAboutStepData = (
     threat,
     isAssociatedToEndpointList,
     isBuildingBlock,
-    isNew,
     note,
     ruleNameOverride,
     timestampOverride,
@@ -180,11 +198,11 @@ export const formatAboutStepData = (
             {
               id: ENDPOINT_LIST_ID,
               list_id: ENDPOINT_LIST_ID,
-              namespace_type: 'agnostic',
-              type: 'endpoint',
+              namespace_type: 'agnostic' as NamespaceType,
+              type: 'endpoint' as ExceptionListType,
             },
             ...detectionExceptionLists,
-          ] as AboutStepRuleJson['exceptions_list'],
+          ],
         }
       : exceptionsList != null
       ? {
@@ -237,16 +255,19 @@ export const formatActionsStepData = (actionsStepData: ActionsStepRule): Actions
   };
 };
 
-export const formatRule = (
+// Used to format form data in rule edit and
+// create flows so "T" here would likely
+// either be CreateRulesSchema or Rule
+export const formatRule = <T>(
   defineStepData: DefineStepRule,
   aboutStepData: AboutStepRule,
   scheduleData: ScheduleStepRule,
   actionsData: ActionsStepRule,
   rule?: Rule | null
-): NewRule =>
-  deepmerge.all([
+): T =>
+  (deepmerge.all([
     formatDefineStepData(defineStepData),
     formatAboutStepData(aboutStepData, rule?.exceptions_list),
     formatScheduleStepData(scheduleData),
     formatActionsStepData(actionsData),
-  ]) as NewRule;
+  ]) as unknown) as T;
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx
index d2eb3228cbbf..48247392dfe7 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx
@@ -9,7 +9,8 @@ import React, { useCallback, useRef, useState, useMemo } from 'react';
 import { useHistory } from 'react-router-dom';
 import styled, { StyledComponent } from 'styled-components';
 
-import { usePersistRule } from '../../../../containers/detection_engine/rules';
+import { useCreateRule } from '../../../../containers/detection_engine/rules';
+import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
 import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config';
 
 import {
@@ -21,32 +22,20 @@ import { displaySuccessToast, useStateToaster } from '../../../../../common/comp
 import { SpyRoute } from '../../../../../common/utils/route/spy_routes';
 import { useUserData } from '../../../../components/user_info';
 import { AccordionTitle } from '../../../../components/rules/accordion_title';
-import { FormData, FormHook } from '../../../../../shared_imports';
-import { StepAboutRule } from '../../../../components/rules/step_about_rule';
 import { StepDefineRule } from '../../../../components/rules/step_define_rule';
+import { StepAboutRule } from '../../../../components/rules/step_about_rule';
 import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule';
 import { StepRuleActions } from '../../../../components/rules/step_rule_actions';
 import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page';
 import * as RuleI18n from '../translations';
 import { redirectToDetections, getActionMessageParams, userHasNoPermissions } from '../helpers';
-import {
-  AboutStepRule,
-  DefineStepRule,
-  RuleStep,
-  RuleStepData,
-  ScheduleStepRule,
-  ActionsStepRule,
-} from '../types';
-import { formatRule } from './helpers';
+import { RuleStep, RuleStepsFormData, RuleStepsFormHooks } from '../types';
+import { formatRule, stepIsValid } from './helpers';
 import * as i18n from './translations';
 import { SecurityPageName } from '../../../../../app/types';
+import { ruleStepsOrder } from '../utils';
 
-const stepsRuleOrder = [
-  RuleStep.defineRule,
-  RuleStep.aboutRule,
-  RuleStep.scheduleRule,
-  RuleStep.ruleActions,
-];
+const formHookNoop = async (): Promise<undefined> => undefined;
 
 const MyEuiPanel = styled(EuiPanel)<{
   zindex?: number;
@@ -99,95 +88,137 @@ const CreateRulePageComponent: React.FC = () => {
   } = useListsConfig();
   const loading = userInfoLoading || listsConfigLoading;
   const [, dispatchToaster] = useStateToaster();
-  const [openAccordionId, setOpenAccordionId] = useState<RuleStep>(RuleStep.defineRule);
+  const [activeStep, setActiveStep] = useState<RuleStep>(RuleStep.defineRule);
+  const getNextStep = (step: RuleStep): RuleStep | undefined =>
+    ruleStepsOrder[ruleStepsOrder.indexOf(step) + 1];
   const defineRuleRef = useRef<EuiAccordion | null>(null);
   const aboutRuleRef = useRef<EuiAccordion | null>(null);
   const scheduleRuleRef = useRef<EuiAccordion | null>(null);
   const ruleActionsRef = useRef<EuiAccordion | null>(null);
-  const stepsForm = useRef<Record<RuleStep, FormHook<FormData> | null>>({
-    [RuleStep.defineRule]: null,
-    [RuleStep.aboutRule]: null,
-    [RuleStep.scheduleRule]: null,
-    [RuleStep.ruleActions]: null,
+  const formHooks = useRef<RuleStepsFormHooks>({
+    [RuleStep.defineRule]: formHookNoop,
+    [RuleStep.aboutRule]: formHookNoop,
+    [RuleStep.scheduleRule]: formHookNoop,
+    [RuleStep.ruleActions]: formHookNoop,
   });
-  const stepsData = useRef<Record<RuleStep, RuleStepData>>({
+  const setFormHook = useCallback(
+    <K extends keyof RuleStepsFormHooks>(step: K, hook: RuleStepsFormHooks[K]) => {
+      formHooks.current[step] = hook;
+    },
+    []
+  );
+  const stepsData = useRef<RuleStepsFormData>({
     [RuleStep.defineRule]: { isValid: false, data: undefined },
     [RuleStep.aboutRule]: { isValid: false, data: undefined },
     [RuleStep.scheduleRule]: { isValid: false, data: undefined },
     [RuleStep.ruleActions]: { isValid: false, data: undefined },
   });
-  const [isStepRuleInReadOnlyView, setIsStepRuleInEditView] = useState<Record<RuleStep, boolean>>({
+  const setStepData = <K extends keyof RuleStepsFormData>(
+    step: K,
+    data: RuleStepsFormData[K]
+  ): void => {
+    stepsData.current[step] = data;
+  };
+  const [openSteps, setOpenSteps] = useState({
     [RuleStep.defineRule]: false,
     [RuleStep.aboutRule]: false,
     [RuleStep.scheduleRule]: false,
     [RuleStep.ruleActions]: false,
   });
-  const [{ isLoading, isSaved }, setRule] = usePersistRule();
-  const actionMessageParams = useMemo(
-    () =>
-      getActionMessageParams((stepsData.current['define-rule'].data as DefineStepRule)?.ruleType),
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [stepsData.current['define-rule'].data]
-  );
+  const [{ isLoading, isSaved }, setRule] = useCreateRule();
+  const ruleType = stepsData.current[RuleStep.defineRule].data?.ruleType;
+  const ruleName = stepsData.current[RuleStep.aboutRule].data?.name;
+  const actionMessageParams = useMemo(() => getActionMessageParams(ruleType), [ruleType]);
   const history = useHistory();
 
-  const setStepData = useCallback(
-    (step: RuleStep, data: unknown, isValid: boolean) => {
-      stepsData.current[step] = { ...stepsData.current[step], data, isValid };
-      if (isValid) {
-        const stepRuleIdx = stepsRuleOrder.findIndex((item) => step === item);
-        if ([0, 1, 2].includes(stepRuleIdx)) {
-          if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) {
-            setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]);
-            setIsStepRuleInEditView({
-              ...isStepRuleInReadOnlyView,
-              [step]: true,
-              [stepsRuleOrder[stepRuleIdx + 1]]: false,
-            });
-          } else if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) {
-            setIsStepRuleInEditView({
-              ...isStepRuleInReadOnlyView,
-              [step]: true,
-            });
-            openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]);
-            setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]);
+  const handleAccordionToggle = useCallback(
+    (step: RuleStep, isOpen: boolean) =>
+      setOpenSteps((_openSteps) => ({
+        ..._openSteps,
+        [step]: isOpen,
+      })),
+    []
+  );
+  const goToStep = useCallback(
+    (step: RuleStep) => {
+      if (ruleStepsOrder.indexOf(step) > ruleStepsOrder.indexOf(activeStep) && !openSteps[step]) {
+        toggleStepAccordion(step);
+      }
+      setActiveStep(step);
+    },
+    [activeStep, openSteps]
+  );
+
+  const toggleStepAccordion = (step: RuleStep | null) => {
+    if (step === RuleStep.defineRule) {
+      defineRuleRef.current?.onToggle();
+    } else if (step === RuleStep.aboutRule) {
+      aboutRuleRef.current?.onToggle();
+    } else if (step === RuleStep.scheduleRule) {
+      scheduleRuleRef.current?.onToggle();
+    } else if (step === RuleStep.ruleActions) {
+      ruleActionsRef.current?.onToggle();
+    }
+  };
+
+  const editStep = useCallback(
+    async (step: RuleStep) => {
+      const activeStepData = await formHooks.current[activeStep]();
+
+      if (activeStepData?.isValid) {
+        setStepData(activeStep, activeStepData);
+        goToStep(step);
+      }
+    },
+    [activeStep, goToStep]
+  );
+  const submitStep = useCallback(
+    async (step: RuleStep) => {
+      const stepData = await formHooks.current[step]();
+
+      if (stepData?.isValid) {
+        setStepData(step, stepData);
+        const nextStep = getNextStep(step);
+
+        if (nextStep != null) {
+          goToStep(nextStep);
+        } else {
+          const defineStep = await stepsData.current[RuleStep.defineRule];
+          const aboutStep = await stepsData.current[RuleStep.aboutRule];
+          const scheduleStep = await stepsData.current[RuleStep.scheduleRule];
+          const actionsStep = await stepsData.current[RuleStep.ruleActions];
+
+          if (
+            stepIsValid(defineStep) &&
+            stepIsValid(aboutStep) &&
+            stepIsValid(scheduleStep) &&
+            stepIsValid(actionsStep)
+          ) {
+            setRule(
+              formatRule<CreateRulesSchema>(
+                defineStep.data,
+                aboutStep.data,
+                scheduleStep.data,
+                actionsStep.data
+              )
+            );
           }
-        } else if (
-          stepRuleIdx === 3 &&
-          stepsData.current[RuleStep.defineRule].isValid &&
-          stepsData.current[RuleStep.aboutRule].isValid &&
-          stepsData.current[RuleStep.scheduleRule].isValid
-        ) {
-          setRule(
-            formatRule(
-              stepsData.current[RuleStep.defineRule].data as DefineStepRule,
-              stepsData.current[RuleStep.aboutRule].data as AboutStepRule,
-              stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule,
-              stepsData.current[RuleStep.ruleActions].data as ActionsStepRule
-            )
-          );
         }
       }
     },
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [isStepRuleInReadOnlyView, openAccordionId, stepsData.current, setRule]
+    [goToStep, setRule]
   );
 
-  const setStepsForm = useCallback((step: RuleStep, form: FormHook<FormData>) => {
-    stepsForm.current[step] = form;
-  }, []);
-
   const getAccordionType = useCallback(
-    (accordionId: RuleStep) => {
-      if (accordionId === openAccordionId) {
+    (step: RuleStep) => {
+      if (step === activeStep) {
         return 'active';
-      } else if (stepsData.current[accordionId].isValid) {
+      } else if (stepsData.current[step].isValid) {
         return 'valid';
       }
       return 'passive';
     },
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [openAccordionId, stepsData.current]
+    [activeStep]
   );
 
   const defineRuleButton = (
@@ -197,7 +228,6 @@ const CreateRulePageComponent: React.FC = () => {
       type={getAccordionType(RuleStep.defineRule)}
     />
   );
-
   const aboutRuleButton = (
     <AccordionTitle
       name="2"
@@ -205,7 +235,6 @@ const CreateRulePageComponent: React.FC = () => {
       type={getAccordionType(RuleStep.aboutRule)}
     />
   );
-
   const scheduleRuleButton = (
     <AccordionTitle
       name="3"
@@ -213,7 +242,6 @@ const CreateRulePageComponent: React.FC = () => {
       type={getAccordionType(RuleStep.scheduleRule)}
     />
   );
-
   const ruleActionsButton = (
     <AccordionTitle
       name="4"
@@ -222,63 +250,7 @@ const CreateRulePageComponent: React.FC = () => {
     />
   );
 
-  const openCloseAccordion = (accordionId: RuleStep | null) => {
-    if (accordionId != null) {
-      if (accordionId === RuleStep.defineRule && defineRuleRef.current != null) {
-        defineRuleRef.current.onToggle();
-      } else if (accordionId === RuleStep.aboutRule && aboutRuleRef.current != null) {
-        aboutRuleRef.current.onToggle();
-      } else if (accordionId === RuleStep.scheduleRule && scheduleRuleRef.current != null) {
-        scheduleRuleRef.current.onToggle();
-      } else if (accordionId === RuleStep.ruleActions && ruleActionsRef.current != null) {
-        ruleActionsRef.current.onToggle();
-      }
-    }
-  };
-
-  const manageAccordions = useCallback(
-    (id: RuleStep, isOpen: boolean) => {
-      const activeRuleIdx = stepsRuleOrder.findIndex((step) => step === openAccordionId);
-      const stepRuleIdx = stepsRuleOrder.findIndex((step) => step === id);
-
-      if ((id === openAccordionId || stepRuleIdx < activeRuleIdx) && !isOpen) {
-        openCloseAccordion(id);
-      } else if (stepRuleIdx >= activeRuleIdx) {
-        if (
-          openAccordionId !== id &&
-          !stepsData.current[openAccordionId].isValid &&
-          !isStepRuleInReadOnlyView[id] &&
-          isOpen
-        ) {
-          openCloseAccordion(id);
-        }
-      }
-    },
-    [isStepRuleInReadOnlyView, openAccordionId, stepsData]
-  );
-
-  const manageIsEditable = useCallback(
-    async (id: RuleStep) => {
-      const activeForm = await stepsForm.current[openAccordionId]?.submit();
-      if (activeForm != null && activeForm?.isValid) {
-        stepsData.current[openAccordionId] = {
-          ...stepsData.current[openAccordionId],
-          data: activeForm.data,
-          isValid: activeForm.isValid,
-        };
-        setOpenAccordionId(id);
-        setIsStepRuleInEditView({
-          ...isStepRuleInReadOnlyView,
-          [openAccordionId]: true,
-          [id]: false,
-        });
-      }
-    },
-    [isStepRuleInReadOnlyView, openAccordionId]
-  );
-
-  if (isSaved) {
-    const ruleName = (stepsData.current[RuleStep.aboutRule].data as AboutStepRule).name;
+  if (isSaved && ruleName) {
     displaySuccessToast(i18n.SUCCESSFULLY_CREATED_RULES(ruleName), dispatchToaster);
     history.replace(getRulesUrl());
     return null;
@@ -319,13 +291,14 @@ const CreateRulePageComponent: React.FC = () => {
             buttonContent={defineRuleButton}
             paddingSize="xs"
             ref={defineRuleRef}
-            onToggle={manageAccordions.bind(null, RuleStep.defineRule)}
+            onToggle={handleAccordionToggle.bind(null, RuleStep.defineRule)}
             extraAction={
               stepsData.current[RuleStep.defineRule].isValid && (
                 <EuiButtonEmpty
+                  data-test-subj="edit-define-rule"
                   iconType="pencil"
                   size="xs"
-                  onClick={manageIsEditable.bind(null, RuleStep.defineRule)}
+                  onClick={() => editStep(RuleStep.defineRule)}
                 >
                   {i18n.EDIT_RULE}
                 </EuiButtonEmpty>
@@ -335,11 +308,11 @@ const CreateRulePageComponent: React.FC = () => {
             <EuiHorizontalRule margin="m" />
             <StepDefineRule
               addPadding={true}
-              defaultValues={stepsData.current[RuleStep.defineRule].data as DefineStepRule}
-              isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.defineRule]}
+              defaultValues={stepsData.current[RuleStep.defineRule].data}
+              isReadOnlyView={activeStep !== RuleStep.defineRule}
               isLoading={isLoading || loading}
-              setForm={setStepsForm}
-              setStepData={setStepData}
+              setForm={setFormHook}
+              onSubmit={() => submitStep(RuleStep.defineRule)}
               descriptionColumns="singleSplit"
             />
           </StepDefineRuleAccordion>
@@ -352,13 +325,14 @@ const CreateRulePageComponent: React.FC = () => {
             buttonContent={aboutRuleButton}
             paddingSize="xs"
             ref={aboutRuleRef}
-            onToggle={manageAccordions.bind(null, RuleStep.aboutRule)}
+            onToggle={handleAccordionToggle.bind(null, RuleStep.aboutRule)}
             extraAction={
               stepsData.current[RuleStep.aboutRule].isValid && (
                 <EuiButtonEmpty
+                  data-test-subj="edit-about-rule"
                   iconType="pencil"
                   size="xs"
-                  onClick={manageIsEditable.bind(null, RuleStep.aboutRule)}
+                  onClick={() => editStep(RuleStep.aboutRule)}
                 >
                   {i18n.EDIT_RULE}
                 </EuiButtonEmpty>
@@ -368,13 +342,13 @@ const CreateRulePageComponent: React.FC = () => {
             <EuiHorizontalRule margin="m" />
             <StepAboutRule
               addPadding={true}
-              defaultValues={stepsData.current[RuleStep.aboutRule].data as AboutStepRule}
-              defineRuleData={stepsData.current[RuleStep.defineRule].data as DefineStepRule}
+              defaultValues={stepsData.current[RuleStep.aboutRule].data}
+              defineRuleData={stepsData.current[RuleStep.defineRule].data}
               descriptionColumns="singleSplit"
-              isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.aboutRule]}
+              isReadOnlyView={activeStep !== RuleStep.aboutRule}
               isLoading={isLoading || loading}
-              setForm={setStepsForm}
-              setStepData={setStepData}
+              setForm={setFormHook}
+              onSubmit={() => submitStep(RuleStep.aboutRule)}
             />
           </EuiAccordion>
         </MyEuiPanel>
@@ -386,13 +360,13 @@ const CreateRulePageComponent: React.FC = () => {
             buttonContent={scheduleRuleButton}
             paddingSize="xs"
             ref={scheduleRuleRef}
-            onToggle={manageAccordions.bind(null, RuleStep.scheduleRule)}
+            onToggle={handleAccordionToggle.bind(null, RuleStep.scheduleRule)}
             extraAction={
               stepsData.current[RuleStep.scheduleRule].isValid && (
                 <EuiButtonEmpty
                   iconType="pencil"
                   size="xs"
-                  onClick={manageIsEditable.bind(null, RuleStep.scheduleRule)}
+                  onClick={() => editStep(RuleStep.scheduleRule)}
                 >
                   {i18n.EDIT_RULE}
                 </EuiButtonEmpty>
@@ -402,12 +376,12 @@ const CreateRulePageComponent: React.FC = () => {
             <EuiHorizontalRule margin="m" />
             <StepScheduleRule
               addPadding={true}
-              defaultValues={stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule}
+              defaultValues={stepsData.current[RuleStep.scheduleRule].data}
               descriptionColumns="singleSplit"
-              isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.scheduleRule]}
+              isReadOnlyView={activeStep !== RuleStep.scheduleRule}
               isLoading={isLoading || loading}
-              setForm={setStepsForm}
-              setStepData={setStepData}
+              setForm={setFormHook}
+              onSubmit={() => submitStep(RuleStep.scheduleRule)}
             />
           </EuiAccordion>
         </MyEuiPanel>
@@ -419,13 +393,13 @@ const CreateRulePageComponent: React.FC = () => {
             buttonContent={ruleActionsButton}
             paddingSize="xs"
             ref={ruleActionsRef}
-            onToggle={manageAccordions.bind(null, RuleStep.ruleActions)}
+            onToggle={handleAccordionToggle.bind(null, RuleStep.ruleActions)}
             extraAction={
               stepsData.current[RuleStep.ruleActions].isValid && (
                 <EuiButtonEmpty
                   iconType="pencil"
                   size="xs"
-                  onClick={manageIsEditable.bind(null, RuleStep.ruleActions)}
+                  onClick={() => editStep(RuleStep.ruleActions)}
                 >
                   {i18n.EDIT_RULE}
                 </EuiButtonEmpty>
@@ -435,10 +409,11 @@ const CreateRulePageComponent: React.FC = () => {
             <EuiHorizontalRule margin="m" />
             <StepRuleActions
               addPadding={true}
-              isReadOnlyView={isStepRuleInReadOnlyView[RuleStep.ruleActions]}
+              defaultValues={stepsData.current[RuleStep.ruleActions].data}
+              isReadOnlyView={activeStep !== RuleStep.ruleActions}
               isLoading={isLoading || loading}
-              setForm={setStepsForm}
-              setStepData={setStepData}
+              setForm={setFormHook}
+              onSubmit={() => submitStep(RuleStep.ruleActions)}
               actionMessageParams={actionMessageParams}
             />
           </EuiAccordion>
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx
index 530222ee1962..5f4fd5966910 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx
@@ -17,7 +17,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
 import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import { useParams, useHistory } from 'react-router-dom';
 
-import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules';
+import { UpdateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
+import { useRule, useUpdateRule } from '../../../../containers/detection_engine/rules';
 import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config';
 import { WrapperPage } from '../../../../../common/components/wrapper_page';
 import {
@@ -28,13 +29,19 @@ import { displaySuccessToast, useStateToaster } from '../../../../../common/comp
 import { SpyRoute } from '../../../../../common/utils/route/spy_routes';
 import { useUserData } from '../../../../components/user_info';
 import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page';
-import { FormHook, FormData } from '../../../../../shared_imports';
 import { StepPanel } from '../../../../components/rules/step_panel';
 import { StepAboutRule } from '../../../../components/rules/step_about_rule';
 import { StepDefineRule } from '../../../../components/rules/step_define_rule';
 import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule';
 import { StepRuleActions } from '../../../../components/rules/step_rule_actions';
-import { formatRule } from '../create/helpers';
+import {
+  formatRule,
+  stepIsValid,
+  isDefineStep,
+  isAboutStep,
+  isScheduleStep,
+  isActionsStep,
+} from '../create/helpers';
 import {
   getStepsData,
   redirectToDetections,
@@ -42,32 +49,12 @@ import {
   userHasNoPermissions,
 } from '../helpers';
 import * as ruleI18n from '../translations';
-import {
-  RuleStep,
-  DefineStepRule,
-  AboutStepRule,
-  ScheduleStepRule,
-  ActionsStepRule,
-} from '../types';
+import { RuleStep, RuleStepsFormHooks, RuleStepsFormData, RuleStepsData } from '../types';
 import * as i18n from './translations';
 import { SecurityPageName } from '../../../../../app/types';
+import { ruleStepsOrder } from '../utils';
 
-interface StepRuleForm {
-  isValid: boolean;
-}
-interface AboutStepRuleForm extends StepRuleForm {
-  data: AboutStepRule | null;
-}
-interface DefineStepRuleForm extends StepRuleForm {
-  data: DefineStepRule | null;
-}
-interface ScheduleStepRuleForm extends StepRuleForm {
-  data: ScheduleStepRule | null;
-}
-
-interface ActionsStepRuleForm extends StepRuleForm {
-  data: ActionsStepRule | null;
-}
+const formHookNoop = async (): Promise<undefined> => undefined;
 
 const EditRulePageComponent: FC = () => {
   const history = useHistory();
@@ -85,49 +72,49 @@ const EditRulePageComponent: FC = () => {
     loading: listsConfigLoading,
     needsConfiguration: needsListsConfiguration,
   } = useListsConfig();
-  const initLoading = userInfoLoading || listsConfigLoading;
   const { detailName: ruleId } = useParams<{ detailName: string | undefined }>();
-  const [loading, rule] = useRule(ruleId);
+  const [ruleLoading, rule] = useRule(ruleId);
+  const loading = ruleLoading || userInfoLoading || listsConfigLoading;
 
-  const [initForm, setInitForm] = useState(false);
-  const [myAboutRuleForm, setMyAboutRuleForm] = useState<AboutStepRuleForm>({
-    data: null,
-    isValid: false,
+  const formHooks = useRef<RuleStepsFormHooks>({
+    [RuleStep.defineRule]: formHookNoop,
+    [RuleStep.aboutRule]: formHookNoop,
+    [RuleStep.scheduleRule]: formHookNoop,
+    [RuleStep.ruleActions]: formHookNoop,
   });
-  const [myDefineRuleForm, setMyDefineRuleForm] = useState<DefineStepRuleForm>({
-    data: null,
-    isValid: false,
+  const stepsData = useRef<RuleStepsFormData>({
+    [RuleStep.defineRule]: { isValid: false, data: undefined },
+    [RuleStep.aboutRule]: { isValid: false, data: undefined },
+    [RuleStep.scheduleRule]: { isValid: false, data: undefined },
+    [RuleStep.ruleActions]: { isValid: false, data: undefined },
   });
-  const [myScheduleRuleForm, setMyScheduleRuleForm] = useState<ScheduleStepRuleForm>({
-    data: null,
-    isValid: false,
+  const defineStep = stepsData.current[RuleStep.defineRule];
+  const aboutStep = stepsData.current[RuleStep.aboutRule];
+  const scheduleStep = stepsData.current[RuleStep.scheduleRule];
+  const actionsStep = stepsData.current[RuleStep.ruleActions];
+  const [activeStep, setActiveStep] = useState<RuleStep>(RuleStep.defineRule);
+  const invalidSteps = ruleStepsOrder.filter((step) => {
+    const stepData = stepsData.current[step];
+    return stepData.data != null && !stepIsValid(stepData);
   });
-  const [myActionsRuleForm, setMyActionsRuleForm] = useState<ActionsStepRuleForm>({
-    data: null,
-    isValid: false,
-  });
-  const [selectedTab, setSelectedTab] = useState<EuiTabbedContentTab>();
-  const stepsForm = useRef<Record<RuleStep, FormHook<FormData> | null>>({
-    [RuleStep.defineRule]: null,
-    [RuleStep.aboutRule]: null,
-    [RuleStep.scheduleRule]: null,
-    [RuleStep.ruleActions]: null,
-  });
-  const [{ isLoading, isSaved }, setRule] = usePersistRule();
-  const [tabHasError, setTabHasError] = useState<RuleStep[]>([]);
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule]);
-  const setStepsForm = useCallback(
-    (step: RuleStep, form: FormHook<FormData>) => {
-      stepsForm.current[step] = form;
-      if (initForm && step === (selectedTab?.id as RuleStep) && form.isSubmitted === false) {
-        setInitForm(false);
-        form.submit();
+  const [{ isLoading, isSaved }, setRule] = useUpdateRule();
+  const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule?.type]);
+  const setFormHook = useCallback(
+    <K extends keyof RuleStepsFormHooks>(step: K, hook: RuleStepsFormHooks[K]) => {
+      formHooks.current[step] = hook;
+      if (step === activeStep) {
+        hook();
       }
     },
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [initForm, selectedTab]
+    [activeStep]
   );
+  const setStepData = useCallback(
+    <K extends keyof RuleStepsData>(step: K, data: RuleStepsData[K], isValid: boolean) => {
+      stepsData.current[step] = { ...stepsData.current[step], data, isValid };
+    },
+    []
+  );
+
   const tabs = useMemo(
     () => [
       {
@@ -137,14 +124,14 @@ const EditRulePageComponent: FC = () => {
         content: (
           <>
             <EuiSpacer />
-            <StepPanel loading={loading || initLoading} title={ruleI18n.DEFINITION}>
-              {myDefineRuleForm.data != null && (
+            <StepPanel loading={loading} title={ruleI18n.DEFINITION}>
+              {defineStep.data != null && (
                 <StepDefineRule
                   isReadOnlyView={false}
                   isLoading={isLoading}
                   isUpdateView
-                  defaultValues={myDefineRuleForm.data}
-                  setForm={setStepsForm}
+                  defaultValues={defineStep.data}
+                  setForm={setFormHook}
                 />
               )}
               <EuiSpacer />
@@ -159,15 +146,15 @@ const EditRulePageComponent: FC = () => {
         content: (
           <>
             <EuiSpacer />
-            <StepPanel loading={loading || initLoading} title={ruleI18n.ABOUT}>
-              {myAboutRuleForm.data != null && myDefineRuleForm.data != null && (
+            <StepPanel loading={loading} title={ruleI18n.ABOUT}>
+              {aboutStep.data != null && defineStep.data != null && (
                 <StepAboutRule
                   isReadOnlyView={false}
                   isLoading={isLoading}
                   isUpdateView
-                  defaultValues={myAboutRuleForm.data}
-                  defineRuleData={myDefineRuleForm.data}
-                  setForm={setStepsForm}
+                  defaultValues={aboutStep.data}
+                  defineRuleData={defineStep.data}
+                  setForm={setFormHook}
                 />
               )}
               <EuiSpacer />
@@ -182,14 +169,14 @@ const EditRulePageComponent: FC = () => {
         content: (
           <>
             <EuiSpacer />
-            <StepPanel loading={loading || initLoading} title={ruleI18n.SCHEDULE}>
-              {myScheduleRuleForm.data != null && (
+            <StepPanel loading={loading} title={ruleI18n.SCHEDULE}>
+              {scheduleStep.data != null && (
                 <StepScheduleRule
                   isReadOnlyView={false}
                   isLoading={isLoading}
                   isUpdateView
-                  defaultValues={myScheduleRuleForm.data}
-                  setForm={setStepsForm}
+                  defaultValues={scheduleStep.data}
+                  setForm={setFormHook}
                 />
               )}
               <EuiSpacer />
@@ -203,14 +190,14 @@ const EditRulePageComponent: FC = () => {
         content: (
           <>
             <EuiSpacer />
-            <StepPanel loading={loading || initLoading} title={ruleI18n.ACTIONS}>
-              {myActionsRuleForm.data != null && (
+            <StepPanel loading={loading} title={ruleI18n.ACTIONS}>
+              {actionsStep.data != null && (
                 <StepRuleActions
                   isReadOnlyView={false}
                   isLoading={isLoading}
                   isUpdateView
-                  defaultValues={myActionsRuleForm.data}
-                  setForm={setStepsForm}
+                  defaultValues={actionsStep.data}
+                  setForm={setFormHook}
                   actionMessageParams={actionMessageParams}
                 />
               )}
@@ -220,76 +207,56 @@ const EditRulePageComponent: FC = () => {
         ),
       },
     ],
-    // eslint-disable-next-line react-hooks/exhaustive-deps
     [
-      rule,
+      rule?.immutable,
       loading,
-      initLoading,
+      defineStep.data,
       isLoading,
-      myAboutRuleForm,
-      myDefineRuleForm,
-      myScheduleRuleForm,
-      myActionsRuleForm,
-      setStepsForm,
-      stepsForm,
+      setFormHook,
+      aboutStep.data,
+      scheduleStep.data,
+      actionsStep.data,
       actionMessageParams,
     ]
   );
 
   const onSubmit = useCallback(async () => {
-    const activeFormId = selectedTab?.id as RuleStep;
-    const activeForm = await stepsForm.current[activeFormId]?.submit();
-
-    const invalidForms = [
-      RuleStep.aboutRule,
-      RuleStep.defineRule,
-      RuleStep.scheduleRule,
-      RuleStep.ruleActions,
-    ].reduce<RuleStep[]>((acc, step) => {
-      if (
-        (step === activeFormId && activeForm != null && !activeForm?.isValid) ||
-        (step === RuleStep.aboutRule && !myAboutRuleForm.isValid) ||
-        (step === RuleStep.defineRule && !myDefineRuleForm.isValid) ||
-        (step === RuleStep.scheduleRule && !myScheduleRuleForm.isValid) ||
-        (step === RuleStep.ruleActions && !myActionsRuleForm.isValid)
-      ) {
-        return [...acc, step];
-      }
-      return acc;
-    }, []);
+    const activeStepData = await formHooks.current[activeStep]();
+    if (activeStepData?.data != null) {
+      setStepData(activeStep, activeStepData.data, activeStepData.isValid);
+    }
+    const define = isDefineStep(activeStepData) ? activeStepData : defineStep;
+    const about = isAboutStep(activeStepData) ? activeStepData : aboutStep;
+    const schedule = isScheduleStep(activeStepData) ? activeStepData : scheduleStep;
+    const actions = isActionsStep(activeStepData) ? activeStepData : actionsStep;
 
-    if (invalidForms.length === 0 && activeForm != null) {
-      setTabHasError([]);
+    if (
+      stepIsValid(define) &&
+      stepIsValid(about) &&
+      stepIsValid(schedule) &&
+      stepIsValid(actions)
+    ) {
       setRule({
-        ...formatRule(
-          (activeFormId === RuleStep.defineRule
-            ? activeForm.data
-            : myDefineRuleForm.data) as DefineStepRule,
-          (activeFormId === RuleStep.aboutRule
-            ? activeForm.data
-            : myAboutRuleForm.data) as AboutStepRule,
-          (activeFormId === RuleStep.scheduleRule
-            ? activeForm.data
-            : myScheduleRuleForm.data) as ScheduleStepRule,
-          (activeFormId === RuleStep.ruleActions
-            ? activeForm.data
-            : myActionsRuleForm.data) as ActionsStepRule,
+        ...formatRule<UpdateRulesSchema>(
+          define.data,
+          about.data,
+          schedule.data,
+          actions.data,
           rule
         ),
         ...(ruleId ? { id: ruleId } : {}),
       });
-    } else {
-      setTabHasError(invalidForms);
     }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [
-    stepsForm,
-    myAboutRuleForm,
-    myDefineRuleForm,
-    myScheduleRuleForm,
-    myActionsRuleForm,
-    selectedTab,
+    aboutStep,
+    actionsStep,
+    activeStep,
+    defineStep,
+    rule,
     ruleId,
+    scheduleStep,
+    setRule,
+    setStepData,
   ]);
 
   useEffect(() => {
@@ -297,48 +264,29 @@ const EditRulePageComponent: FC = () => {
       const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({
         rule,
       });
-      setMyAboutRuleForm({ data: aboutRuleData, isValid: true });
-      setMyDefineRuleForm({ data: defineRuleData, isValid: true });
-      setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true });
-      setMyActionsRuleForm({ data: ruleActionsData, isValid: true });
+      setStepData(RuleStep.defineRule, defineRuleData, true);
+      setStepData(RuleStep.aboutRule, aboutRuleData, true);
+      setStepData(RuleStep.scheduleRule, scheduleRuleData, true);
+      setStepData(RuleStep.ruleActions, ruleActionsData, true);
     }
-  }, [rule]);
+  }, [rule, setStepData]);
+
+  const goToStep = useCallback(async (step: RuleStep) => {
+    setActiveStep(step);
+    await formHooks.current[step]();
+  }, []);
 
   const onTabClick = useCallback(
     async (tab: EuiTabbedContentTab) => {
-      if (selectedTab != null) {
-        const ruleStep = selectedTab.id as RuleStep;
-        const respForm = await stepsForm.current[ruleStep]?.submit();
+      const targetStep = tab.id as RuleStep;
+      const activeStepData = await formHooks.current[activeStep]();
 
-        if (respForm != null) {
-          if (ruleStep === RuleStep.aboutRule) {
-            setMyAboutRuleForm({
-              data: respForm.data as AboutStepRule,
-              isValid: respForm.isValid,
-            });
-          } else if (ruleStep === RuleStep.defineRule) {
-            setMyDefineRuleForm({
-              data: respForm.data as DefineStepRule,
-              isValid: respForm.isValid,
-            });
-          } else if (ruleStep === RuleStep.scheduleRule) {
-            setMyScheduleRuleForm({
-              data: respForm.data as ScheduleStepRule,
-              isValid: respForm.isValid,
-            });
-          } else if (ruleStep === RuleStep.ruleActions) {
-            setMyActionsRuleForm({
-              data: respForm.data as ActionsStepRule,
-              isValid: respForm.isValid,
-            });
-          }
-        }
+      if (activeStepData?.data != null) {
+        setStepData(activeStep, activeStepData.data, activeStepData.isValid);
+        goToStep(targetStep);
       }
-      setInitForm(true);
-      setSelectedTab(tab);
     },
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [selectedTab, stepsForm.current]
+    [activeStep, goToStep, setStepData]
   );
 
   const goToDetailsRule = useCallback(
@@ -350,23 +298,13 @@ const EditRulePageComponent: FC = () => {
   );
 
   useEffect(() => {
-    if (rule != null) {
-      const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({
-        rule,
-      });
-      setMyAboutRuleForm({ data: aboutRuleData, isValid: true });
-      setMyDefineRuleForm({ data: defineRuleData, isValid: true });
-      setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true });
-      setMyActionsRuleForm({ data: ruleActionsData, isValid: true });
+    if (rule?.immutable) {
+      setActiveStep(RuleStep.ruleActions);
+    } else {
+      setActiveStep(RuleStep.defineRule);
     }
   }, [rule]);
 
-  useEffect(() => {
-    const tabIndex = rule?.immutable ? 3 : 0;
-    setSelectedTab(tabs[tabIndex]);
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [rule]);
-
   if (isSaved) {
     displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster);
     history.replace(getRuleDetailsUrl(ruleId ?? ''));
@@ -400,14 +338,14 @@ const EditRulePageComponent: FC = () => {
           isLoading={isLoading}
           title={i18n.PAGE_TITLE}
         />
-        {tabHasError.length > 0 && (
+        {invalidSteps.length > 0 && (
           <EuiCallOut title={i18n.SORRY_ERRORS} color="danger" iconType="alert">
             <FormattedMessage
               id="xpack.securitySolution.detectionEngine.rule.editRule.errorMsgDescription"
               defaultMessage="You have an invalid input in {countError, plural, one {this tab} other {these tabs}}: {tabHasError}"
               values={{
-                countError: tabHasError.length,
-                tabHasError: tabHasError
+                countError: invalidSteps.length,
+                tabHasError: invalidSteps
                   .map((t) => {
                     if (t === RuleStep.aboutRule) {
                       return ruleI18n.ABOUT;
@@ -428,7 +366,7 @@ const EditRulePageComponent: FC = () => {
 
         <EuiTabbedContent
           initialSelectedTab={tabs[0]}
-          selectedTab={tabs.find((t) => t.id === selectedTab?.id)}
+          selectedTab={tabs.find((t) => t.id === activeStep)}
           onTabClick={onTabClick}
           tabs={tabs}
         />
@@ -453,7 +391,7 @@ const EditRulePageComponent: FC = () => {
               onClick={onSubmit}
               iconType="save"
               isLoading={isLoading}
-              isDisabled={initLoading}
+              isDisabled={loading}
             >
               {i18n.SAVE_CHANGES}
             </EuiButton>
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx
index 10a20807d6f8..f11b0ac4ec3f 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx
@@ -43,7 +43,6 @@ describe('rule helpers', () => {
         rule: mockRuleWithEverything('test-id'),
       });
       const defineRuleStepData = {
-        isNew: false,
         ruleType: 'saved_query',
         anomalyThreshold: 50,
         index: ['auditbeat-*'],
@@ -93,7 +92,6 @@ describe('rule helpers', () => {
         falsePositives: ['test'],
         isAssociatedToEndpointList: false,
         isBuildingBlock: false,
-        isNew: false,
         license: 'Elastic License',
         name: 'Query with rule-id',
         note: '# this is some markdown documentation',
@@ -121,11 +119,10 @@ describe('rule helpers', () => {
         ],
         timestampOverride: 'event.ingested',
       };
-      const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false };
+      const scheduleRuleStepData = { from: '0s', interval: '5m' };
       const ruleActionsStepData = {
         enabled: true,
         throttle: 'no_actions',
-        isNew: false,
         actions: [],
       };
       const aboutRuleDataDetailsData = {
@@ -202,7 +199,6 @@ describe('rule helpers', () => {
     test('returns with saved_id if value exists on rule', () => {
       const result: DefineStepRule = getDefineStepsData(mockRule('test-id'));
       const expected = {
-        isNew: false,
         ruleType: 'saved_query',
         anomalyThreshold: 50,
         machineLearningJobId: '',
@@ -235,7 +231,6 @@ describe('rule helpers', () => {
       delete mockedRule.saved_id;
       const result: DefineStepRule = getDefineStepsData(mockedRule);
       const expected = {
-        isNew: false,
         ruleType: 'saved_query',
         anomalyThreshold: 50,
         machineLearningJobId: '',
@@ -311,7 +306,6 @@ describe('rule helpers', () => {
       };
       const result: ScheduleStepRule = getScheduleStepsData(mockedRule);
       const expected = {
-        isNew: false,
         interval: mockedRule.interval,
         from: '0s',
       };
@@ -344,7 +338,6 @@ describe('rule helpers', () => {
           },
         ],
         enabled: mockedRule.enabled,
-        isNew: false,
         throttle: 'no_actions',
       };
 
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
index 8178f5ae5ba1..aab73c5d5a1e 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
@@ -27,6 +27,7 @@ import {
 import {
   SeverityMapping,
   Type,
+  Severity,
 } from '../../../../../common/detection_engine/schemas/common/schemas';
 import { severityOptions } from '../../../components/rules/step_about_rule/data';
 
@@ -67,7 +68,6 @@ export const getActionsStepsData = (
 
   return {
     actions: actions?.map(transformRuleToAlertAction),
-    isNew: false,
     throttle,
     kibanaSiemAppUrl: meta?.kibana_siem_app_url,
     enabled,
@@ -75,7 +75,6 @@ export const getActionsStepsData = (
 };
 
 export const getDefineStepsData = (rule: Rule): DefineStepRule => ({
-  isNew: false,
   ruleType: rule.type,
   anomalyThreshold: rule.anomaly_threshold ?? 50,
   machineLearningJobId: rule.machine_learning_job_id ?? '',
@@ -100,7 +99,6 @@ export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => {
   const fromHumanizedValue = getHumanizedDuration(from, interval);
 
   return {
-    isNew: false,
     interval,
     from: fromHumanizedValue,
   };
@@ -142,7 +140,6 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu
   } = rule;
 
   return {
-    isNew: false,
     author,
     isAssociatedToEndpointList: exceptionsList?.some(({ id }) => id === ENDPOINT_LIST_ID) ?? false,
     isBuildingBlock: buildingBlockType !== undefined,
@@ -154,7 +151,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu
     note: note!,
     references,
     severity: {
-      value: severity,
+      value: severity as Severity,
       mapping: fillEmptySeverityMappings(severityMapping),
       isMappingChecked: severityMapping.length > 0,
     },
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
index 891af4b8ca80..e3d0ea123872 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
@@ -7,7 +7,6 @@
 import { RuleAlertAction } from '../../../../../common/detection_engine/types';
 import { AlertAction } from '../../../../../../alerts/common';
 import { Filter } from '../../../../../../../../src/plugins/data/common';
-import { FormData, FormHook } from '../../../../shared_imports';
 import { FieldValueQueryBar } from '../../../components/rules/query_bar';
 import { FieldValueTimeline } from '../../../components/rules/pick_timeline';
 import { FieldValueThreshold } from '../../../components/rules/threshold_input';
@@ -18,14 +17,16 @@ import {
   RiskScoreMapping,
   RuleNameOverride,
   SeverityMapping,
+  SortOrder,
   TimestampOverride,
   Type,
+  Severity,
 } from '../../../../../common/detection_engine/schemas/common/schemas';
 import { List } from '../../../../../common/detection_engine/schemas/types';
 
 export interface EuiBasicTableSortTypes {
   field: string;
-  direction: 'asc' | 'desc';
+  direction: SortOrder;
 }
 
 export interface EuiBasicTableOnChange {
@@ -36,34 +37,51 @@ export interface EuiBasicTableOnChange {
   sort?: EuiBasicTableSortTypes;
 }
 
+export type RuleStatusType = 'passive' | 'active' | 'valid';
+
 export enum RuleStep {
   defineRule = 'define-rule',
   aboutRule = 'about-rule',
   scheduleRule = 'schedule-rule',
   ruleActions = 'rule-actions',
 }
-export type RuleStatusType = 'passive' | 'active' | 'valid';
+export type RuleStepsOrder = [
+  RuleStep.defineRule,
+  RuleStep.aboutRule,
+  RuleStep.scheduleRule,
+  RuleStep.ruleActions
+];
 
-export interface RuleStepData {
-  data: unknown;
-  isValid: boolean;
+export interface RuleStepsData {
+  [RuleStep.defineRule]: DefineStepRule;
+  [RuleStep.aboutRule]: AboutStepRule;
+  [RuleStep.scheduleRule]: ScheduleStepRule;
+  [RuleStep.ruleActions]: ActionsStepRule;
 }
 
+export type RuleStepsFormData = {
+  [K in keyof RuleStepsData]: {
+    data: RuleStepsData[K] | undefined;
+    isValid: boolean;
+  };
+};
+
+export type RuleStepsFormHooks = {
+  [K in keyof RuleStepsData]: () => Promise<RuleStepsFormData[K] | undefined>;
+};
+
 export interface RuleStepProps {
   addPadding?: boolean;
   descriptionColumns?: 'multi' | 'single' | 'singleSplit';
-  setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void;
   isReadOnlyView: boolean;
   isUpdateView?: boolean;
   isLoading: boolean;
+  onSubmit?: () => void;
   resizeParentContainer?: (height: number) => void;
-  setForm?: (step: RuleStep, form: FormHook<FormData>) => void;
+  setForm?: <K extends keyof RuleStepsFormHooks>(step: K, hook: RuleStepsFormHooks[K]) => void;
 }
 
-interface StepRuleData {
-  isNew: boolean;
-}
-export interface AboutStepRule extends StepRuleData {
+export interface AboutStepRule {
   author: string[];
   name: string;
   description: string;
@@ -87,7 +105,7 @@ export interface AboutStepRuleDetails {
 }
 
 export interface AboutStepSeverity {
-  value: string;
+  value: Severity;
   mapping: SeverityMapping;
   isMappingChecked: boolean;
 }
@@ -98,7 +116,7 @@ export interface AboutStepRiskScore {
   isMappingChecked: boolean;
 }
 
-export interface DefineStepRule extends StepRuleData {
+export interface DefineStepRule {
   anomalyThreshold: number;
   index: string[];
   machineLearningJobId: string;
@@ -108,13 +126,13 @@ export interface DefineStepRule extends StepRuleData {
   threshold: FieldValueThreshold;
 }
 
-export interface ScheduleStepRule extends StepRuleData {
+export interface ScheduleStepRule {
   interval: string;
   from: string;
   to?: string;
 }
 
-export interface ActionsStepRule extends StepRuleData {
+export interface ActionsStepRule {
   actions: AlertAction[];
   enabled: boolean;
   kibanaSiemAppUrl?: string;
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts
index f862a06807e6..890746838b0d 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts
@@ -20,6 +20,14 @@ import { RouteSpyState } from '../../../../common/utils/route/types';
 import { GetUrlForApp } from '../../../../common/components/navigation/types';
 import { SecurityPageName } from '../../../../app/types';
 import { APP_ID } from '../../../../../common/constants';
+import { RuleStep, RuleStepsOrder } from './types';
+
+export const ruleStepsOrder: RuleStepsOrder = [
+  RuleStep.defineRule,
+  RuleStep.aboutRule,
+  RuleStep.scheduleRule,
+  RuleStep.ruleActions,
+];
 
 const getTabBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetUrlForApp) => {
   const tabPath = pathname.split('/')[1];
diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts
index 759b34cd258d..9e60c35b746d 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts
+++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts
@@ -4,9 +4,9 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 import { SearchResponse } from 'elasticsearch';
-import { AuthenticationsStrategyResponse } from '../../../../common/search_strategy/security_solution/hosts/authentications';
+import { HostAuthenticationsStrategyResponse } from '../../../../common/search_strategy/security_solution/hosts/authentications';
 
-export const mockData: { Authentications: AuthenticationsStrategyResponse } = {
+export const mockData: { Authentications: HostAuthenticationsStrategyResponse } = {
   Authentications: {
     rawResponse: {
       aggregations: {
diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx
index 79d83404f8c4..543646940919 100644
--- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx
@@ -12,15 +12,14 @@ import deepEqual from 'fast-deep-equal';
 import { AbortError } from '../../../../../../../src/plugins/data/common';
 
 import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
+import { HostsQueries } from '../../../../common/search_strategy/security_solution';
 import {
-  Direction,
-  DocValueFields,
-  HostPolicyResponseActionStatus,
-  HostsQueries,
-  PageInfoPaginated,
-  AuthenticationsRequestOptions,
-  AuthenticationsStrategyResponse,
+  HostAuthenticationsRequestOptions,
+  HostAuthenticationsStrategyResponse,
   AuthenticationsEdges,
+  PageInfoPaginated,
+  DocValueFields,
+  SortField,
 } from '../../../../common/search_strategy';
 import { ESTermQuery } from '../../../../common/typed_json';
 
@@ -75,7 +74,7 @@ export const useAuthentications = ({
   const defaultIndex = uiSettings.get<string[]>(DEFAULT_INDEX_KEY);
   const [loading, setLoading] = useState(false);
   const [authenticationsRequest, setAuthenticationsRequest] = useState<
-    AuthenticationsRequestOptions
+    HostAuthenticationsRequestOptions
   >({
     defaultIndex,
     docValueFields: docValueFields ?? [],
@@ -87,10 +86,7 @@ export const useAuthentications = ({
       from: startDate,
       to: endDate,
     },
-    sort: {
-      direction: Direction.desc,
-      field: HostPolicyResponseActionStatus.success,
-    },
+    sort: {} as SortField,
   });
 
   const wrappedLoadMore = useCallback(
@@ -125,14 +121,14 @@ export const useAuthentications = ({
   });
 
   const authenticationsSearch = useCallback(
-    (request: AuthenticationsRequestOptions) => {
+    (request: HostAuthenticationsRequestOptions) => {
       let didCancel = false;
       const asyncSearch = async () => {
         abortCtrl.current = new AbortController();
         setLoading(true);
 
         const searchSubscription$ = data.search
-          .search<AuthenticationsRequestOptions, AuthenticationsStrategyResponse>(request, {
+          .search<HostAuthenticationsRequestOptions, HostAuthenticationsStrategyResponse>(request, {
             strategy: 'securitySolutionSearchStrategy',
             abortSignal: abortCtrl.current.signal,
           })
diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx
index f8e5b1bed73c..82f5a97e9e41 100644
--- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx
@@ -4,36 +4,39 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { getOr } from 'lodash/fp';
-import React from 'react';
-import { Query } from 'react-apollo';
-import { connect, ConnectedProps } from 'react-redux';
-import { compose } from 'redux';
+import deepEqual from 'fast-deep-equal';
+import { noop } from 'lodash/fp';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { useSelector } from 'react-redux';
+
+import { AbortError } from '../../../../../../../src/plugins/data/common';
 
 import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
-import {
-  GetUncommonProcessesQuery,
-  PageInfoPaginated,
-  UncommonProcessesEdges,
-} from '../../../graphql/types';
-import { inputsModel, State, inputsSelectors } from '../../../common/store';
-import { withKibana, WithKibanaProps } from '../../../common/lib/kibana';
+import { PageInfoPaginated, UncommonProcessesEdges } from '../../../graphql/types';
+import { inputsModel, State } from '../../../common/store';
+import { useKibana } from '../../../common/lib/kibana';
 import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers';
-import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers';
-import {
-  QueryTemplatePaginated,
-  QueryTemplatePaginatedProps,
-} from '../../../common/containers/query_template_paginated';
+import { createFilter } from '../../../common/containers/helpers';
+
 import { hostsModel, hostsSelectors } from '../../store';
-import { uncommonProcessesQuery } from './index.gql_query';
+import {
+  HostUncommonProcessesRequestOptions,
+  HostUncommonProcessesStrategyResponse,
+} from '../../../../common/search_strategy/security_solution/hosts/uncommon_processes';
+import { HostsQueries } from '../../../../common/search_strategy/security_solution/hosts';
+import { DocValueFields, SortField } from '../../../../common/search_strategy';
+
+import * as i18n from './translations';
+import { ESTermQuery } from '../../../../common/typed_json';
+import { getInspectResponse } from '../../../helpers';
+import { InspectResponse } from '../../../types';
 
 const ID = 'uncommonProcessesQuery';
 
 export interface UncommonProcessesArgs {
   id: string;
-  inspect: inputsModel.InspectQuery;
+  inspect: InspectResponse;
   isInspected: boolean;
-  loading: boolean;
   loadPage: (newActivePage: number) => void;
   pageInfo: PageInfoPaginated;
   refetch: inputsModel.Refetch;
@@ -41,111 +44,164 @@ export interface UncommonProcessesArgs {
   uncommonProcesses: UncommonProcessesEdges[];
 }
 
-export interface OwnProps extends QueryTemplatePaginatedProps {
-  children: (args: UncommonProcessesArgs) => React.ReactNode;
+interface UseUncommonProcesses {
+  docValueFields?: DocValueFields[];
+  filterQuery?: ESTermQuery | string;
+  endDate: string;
+  skip?: boolean;
+  startDate: string;
   type: hostsModel.HostsType;
 }
 
-type UncommonProcessesProps = OwnProps & PropsFromRedux & WithKibanaProps;
-
-class UncommonProcessesComponentQuery extends QueryTemplatePaginated<
-  UncommonProcessesProps,
-  GetUncommonProcessesQuery.Query,
-  GetUncommonProcessesQuery.Variables
-> {
-  public render() {
-    const {
-      activePage,
-      children,
-      endDate,
-      filterQuery,
-      id = ID,
-      isInspected,
-      kibana,
-      limit,
-      skip,
-      sourceId,
-      startDate,
-    } = this.props;
-    const variables: GetUncommonProcessesQuery.Variables = {
-      defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY),
-      filterQuery: createFilter(filterQuery),
-      inspect: isInspected,
-      pagination: generateTablePaginationOptions(activePage, limit),
-      sourceId,
-      timerange: {
-        interval: '12h',
-        from: startDate!,
-        to: endDate!,
+export const useUncommonProcesses = ({
+  docValueFields,
+  filterQuery,
+  endDate,
+  skip = false,
+  startDate,
+  type,
+}: UseUncommonProcesses): [boolean, UncommonProcessesArgs] => {
+  const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector();
+  const { activePage, limit } = useSelector((state: State) =>
+    getUncommonProcessesSelector(state, type)
+  );
+  const { data, notifications, uiSettings } = useKibana().services;
+  const refetch = useRef<inputsModel.Refetch>(noop);
+  const abortCtrl = useRef(new AbortController());
+  const defaultIndex = uiSettings.get<string[]>(DEFAULT_INDEX_KEY);
+  const [loading, setLoading] = useState(false);
+  const [uncommonProcessesRequest, setUncommonProcessesRequest] = useState<
+    HostUncommonProcessesRequestOptions
+  >({
+    defaultIndex,
+    docValueFields: docValueFields ?? [],
+    factoryQueryType: HostsQueries.uncommonProcesses,
+    filterQuery: createFilter(filterQuery),
+    pagination: generateTablePaginationOptions(activePage, limit),
+    timerange: {
+      interval: '12h',
+      from: startDate!,
+      to: endDate!,
+    },
+    sort: {} as SortField,
+  });
+
+  const wrappedLoadMore = useCallback(
+    (newActivePage: number) => {
+      setUncommonProcessesRequest((prevRequest) => {
+        return {
+          ...prevRequest,
+          pagination: generateTablePaginationOptions(newActivePage, limit),
+        };
+      });
+    },
+    [limit]
+  );
+
+  const [uncommonProcessesResponse, setUncommonProcessesResponse] = useState<UncommonProcessesArgs>(
+    {
+      uncommonProcesses: [],
+      id: ID,
+      inspect: {
+        dsl: [],
+        response: [],
       },
-    };
-    return (
-      <Query<GetUncommonProcessesQuery.Query, GetUncommonProcessesQuery.Variables>
-        query={uncommonProcessesQuery}
-        fetchPolicy={getDefaultFetchPolicy()}
-        notifyOnNetworkStatusChange
-        skip={skip}
-        variables={variables}
-      >
-        {({ data, loading, fetchMore, networkStatus, refetch }) => {
-          const uncommonProcesses = getOr([], 'source.UncommonProcesses.edges', data);
-          this.setFetchMore(fetchMore);
-          this.setFetchMoreOptions((newActivePage: number) => ({
-            variables: {
-              pagination: generateTablePaginationOptions(newActivePage, limit),
+      isInspected: false,
+      loadPage: wrappedLoadMore,
+      pageInfo: {
+        activePage: 0,
+        fakeTotalCount: 0,
+        showMorePagesIndicator: false,
+      },
+      refetch: refetch.current,
+      totalCount: -1,
+    }
+  );
+
+  const uncommonProcessesSearch = useCallback(
+    (request: HostUncommonProcessesRequestOptions) => {
+      let didCancel = false;
+      const asyncSearch = async () => {
+        abortCtrl.current = new AbortController();
+        setLoading(true);
+
+        const searchSubscription$ = data.search
+          .search<HostUncommonProcessesRequestOptions, HostUncommonProcessesStrategyResponse>(
+            request,
+            {
+              strategy: 'securitySolutionSearchStrategy',
+              abortSignal: abortCtrl.current.signal,
+            }
+          )
+          .subscribe({
+            next: (response) => {
+              if (!response.isPartial && !response.isRunning) {
+                if (!didCancel) {
+                  setLoading(false);
+                  setUncommonProcessesResponse((prevResponse) => ({
+                    ...prevResponse,
+                    uncommonProcesses: response.edges,
+                    inspect: getInspectResponse(response, prevResponse.inspect),
+                    pageInfo: response.pageInfo,
+                    refetch: refetch.current,
+                    totalCount: response.totalCount,
+                  }));
+                }
+                searchSubscription$.unsubscribe();
+              } else if (response.isPartial && !response.isRunning) {
+                if (!didCancel) {
+                  setLoading(false);
+                }
+                notifications.toasts.addWarning(i18n.ERROR_UNCOMMON_PROCESSES);
+                searchSubscription$.unsubscribe();
+              }
             },
-            updateQuery: (prev, { fetchMoreResult }) => {
-              if (!fetchMoreResult) {
-                return prev;
+            error: (msg) => {
+              if (!(msg instanceof AbortError)) {
+                notifications.toasts.addDanger({
+                  title: i18n.FAIL_UNCOMMON_PROCESSES,
+                  text: msg.message,
+                });
               }
-              return {
-                ...fetchMoreResult,
-                source: {
-                  ...fetchMoreResult.source,
-                  UncommonProcesses: {
-                    ...fetchMoreResult.source.UncommonProcesses,
-                    edges: [...fetchMoreResult.source.UncommonProcesses.edges],
-                  },
-                },
-              };
             },
-          }));
-          const isLoading = this.isItAValidLoading(loading, variables, networkStatus);
-          return children({
-            id,
-            inspect: getOr(null, 'source.UncommonProcesses.inspect', data),
-            isInspected,
-            loading: isLoading,
-            loadPage: this.wrappedLoadMore,
-            pageInfo: getOr({}, 'source.UncommonProcesses.pageInfo', data),
-            refetch: this.memoizedRefetchQuery(variables, limit, refetch),
-            totalCount: getOr(-1, 'source.UncommonProcesses.totalCount', data),
-            uncommonProcesses,
           });
-        }}
-      </Query>
-    );
-  }
-}
+      };
+      abortCtrl.current.abort();
+      asyncSearch();
+      refetch.current = asyncSearch;
+      return () => {
+        didCancel = true;
+        abortCtrl.current.abort();
+      };
+    },
+    [data.search, notifications.toasts]
+  );
 
-const makeMapStateToProps = () => {
-  const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector();
-  const getQuery = inputsSelectors.globalQueryByIdSelector();
-  const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => {
-    const { isInspected } = getQuery(state, id);
-    return {
-      ...getUncommonProcessesSelector(state, type),
-      isInspected,
-    };
-  };
-  return mapStateToProps;
-};
+  useEffect(() => {
+    setUncommonProcessesRequest((prevRequest) => {
+      const myRequest = {
+        ...prevRequest,
+        defaultIndex,
+        docValueFields: docValueFields ?? [],
+        filterQuery: createFilter(filterQuery),
+        pagination: generateTablePaginationOptions(activePage, limit),
+        timerange: {
+          interval: '12h',
+          from: startDate,
+          to: endDate,
+        },
+        sort: {} as SortField,
+      };
+      if (!skip && !deepEqual(prevRequest, myRequest)) {
+        return myRequest;
+      }
+      return prevRequest;
+    });
+  }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, skip, startDate]);
 
-const connector = connect(makeMapStateToProps);
+  useEffect(() => {
+    uncommonProcessesSearch(uncommonProcessesRequest);
+  }, [uncommonProcessesRequest, uncommonProcessesSearch]);
 
-type PropsFromRedux = ConnectedProps<typeof connector>;
-
-export const UncommonProcessesQuery = compose<React.ComponentClass<OwnProps>>(
-  connector,
-  withKibana
-)(UncommonProcessesComponentQuery);
+  return [loading, uncommonProcessesResponse];
+};
diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/translations.ts
new file mode 100644
index 000000000000..d563d90dfb26
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/translations.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const ERROR_UNCOMMON_PROCESSES = i18n.translate(
+  'xpack.securitySolution.uncommonProcesses.errorSearchDescription',
+  {
+    defaultMessage: `An error has occurred on uncommon processes search`,
+  }
+);
+
+export const FAIL_UNCOMMON_PROCESSES = i18n.translate(
+  'xpack.securitySolution.uncommonProcesses.failSearchDescription',
+  {
+    defaultMessage: `Failed to run search on uncommon processes`,
+  }
+);
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx
index f1691dbaa04b..713958f05a3d 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx
@@ -6,7 +6,7 @@
 
 import { getOr } from 'lodash/fp';
 import React from 'react';
-import { UncommonProcessesQuery } from '../../containers/uncommon_processes';
+import { useUncommonProcesses } from '../../containers/uncommon_processes';
 import { HostsComponentsQueryProps } from './types';
 import { UncommonProcessTable } from '../../components/uncommon_process_table';
 import { manageQuery } from '../../../common/components/page/manage_query';
@@ -15,49 +15,35 @@ const UncommonProcessTableManage = manageQuery(UncommonProcessTable);
 
 export const UncommonProcessQueryTabBody = ({
   deleteQuery,
+  docValueFields,
   endDate,
   filterQuery,
   skip,
   setQuery,
   startDate,
   type,
-}: HostsComponentsQueryProps) => (
-  <UncommonProcessesQuery
-    endDate={endDate}
-    filterQuery={filterQuery}
-    skip={skip}
-    sourceId="default"
-    startDate={startDate}
-    type={type}
-  >
-    {({
-      uncommonProcesses,
-      totalCount,
-      loading,
-      pageInfo,
-      loadPage,
-      id,
-      inspect,
-      isInspected,
-      refetch,
-    }) => (
-      <UncommonProcessTableManage
-        deleteQuery={deleteQuery}
-        data={uncommonProcesses}
-        fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
-        id={id}
-        inspect={inspect}
-        isInspect={isInspected}
-        loading={loading}
-        loadPage={loadPage}
-        refetch={refetch}
-        setQuery={setQuery}
-        showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
-        totalCount={totalCount}
-        type={type}
-      />
-    )}
-  </UncommonProcessesQuery>
-);
+}: HostsComponentsQueryProps) => {
+  const [
+    loading,
+    { uncommonProcesses, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch },
+  ] = useUncommonProcesses({ docValueFields, endDate, filterQuery, skip, startDate, type });
+  return (
+    <UncommonProcessTableManage
+      deleteQuery={deleteQuery}
+      data={uncommonProcesses}
+      fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
+      id={id}
+      inspect={inspect}
+      isInspect={isInspected}
+      loading={loading}
+      loadPage={loadPage}
+      refetch={refetch}
+      setQuery={setQuery}
+      showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
+      totalCount={totalCount}
+      type={type}
+    />
+  );
+};
 
 UncommonProcessQueryTabBody.dispalyName = 'UncommonProcessQueryTabBody';
diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx
index 6bed779d4963..747f5e4f502d 100644
--- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx
@@ -50,14 +50,12 @@ interface UseNetworkTopCountries {
   endDate: string;
   startDate: string;
   skip: boolean;
-  id?: string;
 }
 
 export const useNetworkTopCountries = ({
   endDate,
   filterQuery,
   flowTarget,
-  id = ID,
   skip,
   startDate,
   type,
@@ -101,7 +99,7 @@ export const useNetworkTopCountries = ({
     NetworkTopCountriesArgs
   >({
     networkTopCountries: [],
-    id: ID,
+    id: `${ID}-${flowTarget}`,
     inspect: {
       dsl: [],
       response: [],
diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx
index 770574b0813c..cc0da816c57e 100644
--- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx
+++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx
@@ -4,161 +4,196 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { getOr } from 'lodash/fp';
-import React from 'react';
-import { Query } from 'react-apollo';
-import { connect } from 'react-redux';
-import { compose } from 'redux';
+import { noop } from 'lodash/fp';
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { shallowEqual, useSelector } from 'react-redux';
+import deepEqual from 'fast-deep-equal';
 
+import { ESTermQuery } from '../../../../common/typed_json';
 import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
+import { inputsModel, State } from '../../../common/store';
+import { useKibana } from '../../../common/lib/kibana';
+import { createFilter } from '../../../common/containers/helpers';
+import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers';
+import { networkModel, networkSelectors } from '../../store';
 import {
   FlowTargetSourceDest,
-  GetNetworkTopNFlowQuery,
+  NetworkQueries,
   NetworkTopNFlowEdges,
-  NetworkTopTablesSortField,
+  NetworkTopNFlowRequestOptions,
+  NetworkTopNFlowStrategyResponse,
   PageInfoPaginated,
-} from '../../../graphql/types';
-import { withKibana, WithKibanaProps } from '../../../common/lib/kibana';
-import { inputsModel, inputsSelectors, State } from '../../../common/store';
-import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers';
-import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers';
-import {
-  QueryTemplatePaginated,
-  QueryTemplatePaginatedProps,
-} from '../../../common/containers/query_template_paginated';
-import { networkTopNFlowQuery } from './index.gql_query';
-import { networkModel, networkSelectors } from '../../store';
+} from '../../../../common/search_strategy';
+import { AbortError } from '../../../../../../../src/plugins/data/common';
+import { getInspectResponse } from '../../../helpers';
+import { InspectResponse } from '../../../types';
+import * as i18n from './translations';
 
 const ID = 'networkTopNFlowQuery';
 
 export interface NetworkTopNFlowArgs {
   id: string;
-  ip?: string;
-  inspect: inputsModel.InspectQuery;
+  inspect: InspectResponse;
   isInspected: boolean;
-  loading: boolean;
   loadPage: (newActivePage: number) => void;
-  networkTopNFlow: NetworkTopNFlowEdges[];
   pageInfo: PageInfoPaginated;
   refetch: inputsModel.Refetch;
+  networkTopNFlow: NetworkTopNFlowEdges[];
   totalCount: number;
 }
 
-export interface OwnProps extends QueryTemplatePaginatedProps {
-  children: (args: NetworkTopNFlowArgs) => React.ReactNode;
+interface UseNetworkTopNFlow {
   flowTarget: FlowTargetSourceDest;
   ip?: string;
   type: networkModel.NetworkType;
+  filterQuery?: ESTermQuery | string;
+  endDate: string;
+  startDate: string;
+  skip: boolean;
 }
 
-export interface NetworkTopNFlowComponentReduxProps {
-  activePage: number;
-  isInspected: boolean;
-  limit: number;
-  sort: NetworkTopTablesSortField;
-}
+export const useNetworkTopNFlow = ({
+  endDate,
+  filterQuery,
+  flowTarget,
+  skip,
+  startDate,
+  type,
+}: UseNetworkTopNFlow): [boolean, NetworkTopNFlowArgs] => {
+  const getTopNFlowSelector = networkSelectors.topNFlowSelector();
+  const { activePage, limit, sort } = useSelector(
+    (state: State) => getTopNFlowSelector(state, type, flowTarget),
+    shallowEqual
+  );
+  const { data, notifications, uiSettings } = useKibana().services;
+  const refetch = useRef<inputsModel.Refetch>(noop);
+  const abortCtrl = useRef(new AbortController());
+  const defaultIndex = uiSettings.get<string[]>(DEFAULT_INDEX_KEY);
+  const [loading, setLoading] = useState(false);
+
+  const [networkTopNFlowRequest, setTopNFlowRequest] = useState<NetworkTopNFlowRequestOptions>({
+    defaultIndex,
+    factoryQueryType: NetworkQueries.topNFlow,
+    filterQuery: createFilter(filterQuery),
+    flowTarget,
+    pagination: generateTablePaginationOptions(activePage, limit),
+    sort,
+    timerange: {
+      interval: '12h',
+      from: startDate ? startDate : '',
+      to: endDate ? endDate : new Date(Date.now()).toISOString(),
+    },
+  });
+
+  const wrappedLoadMore = useCallback(
+    (newActivePage: number) => {
+      setTopNFlowRequest((prevRequest) => ({
+        ...prevRequest,
+        pagination: generateTablePaginationOptions(newActivePage, limit),
+      }));
+    },
+    [limit]
+  );
 
-type NetworkTopNFlowProps = OwnProps & NetworkTopNFlowComponentReduxProps & WithKibanaProps;
+  const [networkTopNFlowResponse, setNetworkTopNFlowResponse] = useState<NetworkTopNFlowArgs>({
+    networkTopNFlow: [],
+    id: `${ID}-${flowTarget}`,
+    inspect: {
+      dsl: [],
+      response: [],
+    },
+    isInspected: false,
+    loadPage: wrappedLoadMore,
+    pageInfo: {
+      activePage: 0,
+      fakeTotalCount: 0,
+      showMorePagesIndicator: false,
+    },
+    refetch: refetch.current,
+    totalCount: -1,
+  });
 
-class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated<
-  NetworkTopNFlowProps,
-  GetNetworkTopNFlowQuery.Query,
-  GetNetworkTopNFlowQuery.Variables
-> {
-  public render() {
-    const {
-      activePage,
-      children,
-      endDate,
-      flowTarget,
-      filterQuery,
-      kibana,
-      id = `${ID}-${flowTarget}`,
-      ip,
-      isInspected,
-      limit,
-      skip,
-      sourceId,
-      startDate,
-      sort,
-    } = this.props;
-    const variables: GetNetworkTopNFlowQuery.Variables = {
-      defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY),
-      filterQuery: createFilter(filterQuery),
-      flowTarget,
-      inspect: isInspected,
-      ip,
-      pagination: generateTablePaginationOptions(activePage, limit),
-      sort,
-      sourceId,
-      timerange: {
-        interval: '12h',
-        from: startDate!,
-        to: endDate!,
-      },
-    };
-    return (
-      <Query<GetNetworkTopNFlowQuery.Query, GetNetworkTopNFlowQuery.Variables>
-        fetchPolicy={getDefaultFetchPolicy()}
-        notifyOnNetworkStatusChange
-        query={networkTopNFlowQuery}
-        skip={skip}
-        variables={variables}
-      >
-        {({ data, loading, fetchMore, networkStatus, refetch }) => {
-          const networkTopNFlow = getOr([], `source.NetworkTopNFlow.edges`, data);
-          this.setFetchMore(fetchMore);
-          this.setFetchMoreOptions((newActivePage: number) => ({
-            variables: {
-              pagination: generateTablePaginationOptions(newActivePage, limit),
+  const networkTopNFlowSearch = useCallback(
+    (request: NetworkTopNFlowRequestOptions) => {
+      let didCancel = false;
+      const asyncSearch = async () => {
+        abortCtrl.current = new AbortController();
+        setLoading(true);
+
+        const searchSubscription$ = data.search
+          .search<NetworkTopNFlowRequestOptions, NetworkTopNFlowStrategyResponse>(request, {
+            strategy: 'securitySolutionSearchStrategy',
+            abortSignal: abortCtrl.current.signal,
+          })
+          .subscribe({
+            next: (response) => {
+              if (!response.isPartial && !response.isRunning) {
+                if (!didCancel) {
+                  setLoading(false);
+                  setNetworkTopNFlowResponse((prevResponse) => ({
+                    ...prevResponse,
+                    networkTopNFlow: response.edges,
+                    inspect: getInspectResponse(response, prevResponse.inspect),
+                    pageInfo: response.pageInfo,
+                    refetch: refetch.current,
+                    totalCount: response.totalCount,
+                  }));
+                }
+                searchSubscription$.unsubscribe();
+              } else if (response.isPartial && !response.isRunning) {
+                if (!didCancel) {
+                  setLoading(false);
+                }
+                // TODO: Make response error status clearer
+                notifications.toasts.addWarning(i18n.ERROR_NETWORK_TOP_N_FLOW);
+                searchSubscription$.unsubscribe();
+              }
             },
-            updateQuery: (prev, { fetchMoreResult }) => {
-              if (!fetchMoreResult) {
-                return prev;
+            error: (msg) => {
+              if (!(msg instanceof AbortError)) {
+                notifications.toasts.addDanger({
+                  title: i18n.FAIL_NETWORK_TOP_N_FLOW,
+                  text: msg.message,
+                });
               }
-              return {
-                ...fetchMoreResult,
-                source: {
-                  ...fetchMoreResult.source,
-                  NetworkTopNFlow: {
-                    ...fetchMoreResult.source.NetworkTopNFlow,
-                    edges: [...fetchMoreResult.source.NetworkTopNFlow.edges],
-                  },
-                },
-              };
             },
-          }));
-          const isLoading = this.isItAValidLoading(loading, variables, networkStatus);
-          return children({
-            id,
-            inspect: getOr(null, 'source.NetworkTopNFlow.inspect', data),
-            isInspected,
-            loading: isLoading,
-            loadPage: this.wrappedLoadMore,
-            networkTopNFlow,
-            pageInfo: getOr({}, 'source.NetworkTopNFlow.pageInfo', data),
-            refetch: this.memoizedRefetchQuery(variables, limit, refetch),
-            totalCount: getOr(-1, 'source.NetworkTopNFlow.totalCount', data),
           });
-        }}
-      </Query>
-    );
-  }
-}
+      };
+      abortCtrl.current.abort();
+      asyncSearch();
+      refetch.current = asyncSearch;
+      return () => {
+        didCancel = true;
+        abortCtrl.current.abort();
+      };
+    },
+    [data.search, notifications.toasts]
+  );
 
-const makeMapStateToProps = () => {
-  const getTopNFlowSelector = networkSelectors.topNFlowSelector();
-  const getQuery = inputsSelectors.globalQueryByIdSelector();
-  return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => {
-    const { isInspected } = getQuery(state, id);
-    return {
-      ...getTopNFlowSelector(state, type, flowTarget),
-      isInspected,
-    };
-  };
-};
+  useEffect(() => {
+    setTopNFlowRequest((prevRequest) => {
+      const myRequest = {
+        ...prevRequest,
+        defaultIndex,
+        filterQuery: createFilter(filterQuery),
+        pagination: generateTablePaginationOptions(activePage, limit),
+        timerange: {
+          interval: '12h',
+          from: startDate,
+          to: endDate,
+        },
+        sort,
+      };
+      if (!skip && !deepEqual(prevRequest, myRequest)) {
+        return myRequest;
+      }
+      return prevRequest;
+    });
+  }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]);
 
-export const NetworkTopNFlowQuery = compose<React.ComponentClass<OwnProps>>(
-  connect(makeMapStateToProps),
-  withKibana
-)(NetworkTopNFlowComponentQuery);
+  useEffect(() => {
+    networkTopNFlowSearch(networkTopNFlowRequest);
+  }, [networkTopNFlowRequest, networkTopNFlowSearch]);
+
+  return [loading, networkTopNFlowResponse];
+};
diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/translations.ts b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/translations.ts
new file mode 100644
index 000000000000..4ea704571cf2
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/translations.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const ERROR_NETWORK_TOP_N_FLOW = i18n.translate(
+  'xpack.securitySolution.networkTopNFlow.errorSearchDescription',
+  {
+    defaultMessage: `An error has occurred on network top n flow search`,
+  }
+);
+
+export const FAIL_NETWORK_TOP_N_FLOW = i18n.translate(
+  'xpack.securitySolution.networkTopNFlow.failSearchDescription',
+  {
+    defaultMessage: `Failed to run search on network top n flow`,
+  }
+);
diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_n_flow_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_n_flow_query_table.tsx
index 158b4057a7d5..821452201b78 100644
--- a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_n_flow_query_table.tsx
+++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_n_flow_query_table.tsx
@@ -8,7 +8,7 @@ import { getOr } from 'lodash/fp';
 import React from 'react';
 import { manageQuery } from '../../../common/components/page/manage_query';
 import { NetworkTopNFlowTable } from '../../components/network_top_n_flow_table';
-import { NetworkTopNFlowQuery } from '../../containers/network_top_n_flow';
+import { useNetworkTopNFlow } from '../../containers/network_top_n_flow';
 import { NetworkWithIndexComponentsQueryTableProps } from './types';
 
 const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable);
@@ -22,45 +22,37 @@ export const NetworkTopNFlowQueryTable = ({
   skip,
   startDate,
   type,
-}: NetworkWithIndexComponentsQueryTableProps) => (
-  <NetworkTopNFlowQuery
-    endDate={endDate}
-    filterQuery={filterQuery}
-    flowTarget={flowTarget}
-    ip={ip}
-    skip={skip}
-    sourceId="default"
-    startDate={startDate}
-    type={type}
-  >
-    {({
-      id,
-      inspect,
-      isInspected,
-      loading,
-      loadPage,
-      networkTopNFlow,
-      pageInfo,
-      refetch,
-      totalCount,
-    }) => (
-      <NetworkTopNFlowTableManage
-        data={networkTopNFlow}
-        fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
-        flowTargeted={flowTarget}
-        id={id}
-        inspect={inspect}
-        isInspect={isInspected}
-        loading={loading}
-        loadPage={loadPage}
-        refetch={refetch}
-        setQuery={setQuery}
-        showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
-        totalCount={totalCount}
-        type={type}
-      />
-    )}
-  </NetworkTopNFlowQuery>
-);
+}: NetworkWithIndexComponentsQueryTableProps) => {
+  const [
+    loading,
+    { id, inspect, isInspected, loadPage, networkTopNFlow, pageInfo, refetch, totalCount },
+  ] = useNetworkTopNFlow({
+    endDate,
+    filterQuery,
+    flowTarget,
+    ip,
+    skip,
+    startDate,
+    type,
+  });
+
+  return (
+    <NetworkTopNFlowTableManage
+      data={networkTopNFlow}
+      fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
+      flowTargeted={flowTarget}
+      id={id}
+      inspect={inspect}
+      isInspect={isInspected}
+      loading={loading}
+      loadPage={loadPage}
+      refetch={refetch}
+      setQuery={setQuery}
+      showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
+      totalCount={totalCount}
+      type={type}
+    />
+  );
+};
 
 NetworkTopNFlowQueryTable.displayName = 'NetworkTopNFlowQueryTable';
diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx
index a9f4d504847a..c83bf6ff8090 100644
--- a/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx
+++ b/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx
@@ -8,7 +8,7 @@ import React from 'react';
 import { getOr } from 'lodash/fp';
 
 import { NetworkTopNFlowTable } from '../../components/network_top_n_flow_table';
-import { NetworkTopNFlowQuery } from '../../containers/network_top_n_flow';
+import { useNetworkTopNFlow } from '../../containers/network_top_n_flow';
 import { networkModel } from '../../store';
 import { manageQuery } from '../../../common/components/page/manage_query';
 
@@ -23,44 +23,36 @@ export const IPsQueryTabBody = ({
   startDate,
   setQuery,
   flowTarget,
-}: IPsQueryTabBodyProps) => (
-  <NetworkTopNFlowQuery
-    endDate={endDate}
-    flowTarget={flowTarget}
-    filterQuery={filterQuery}
-    skip={skip}
-    sourceId="default"
-    startDate={startDate}
-    type={networkModel.NetworkType.page}
-  >
-    {({
-      id,
-      inspect,
-      isInspected,
-      loading,
-      loadPage,
-      networkTopNFlow,
-      pageInfo,
-      refetch,
-      totalCount,
-    }) => (
-      <NetworkTopNFlowTableManage
-        data={networkTopNFlow}
-        fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
-        flowTargeted={flowTarget}
-        id={id}
-        inspect={inspect}
-        isInspect={isInspected}
-        loading={loading}
-        loadPage={loadPage}
-        refetch={refetch}
-        setQuery={setQuery}
-        showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
-        totalCount={totalCount}
-        type={networkModel.NetworkType.page}
-      />
-    )}
-  </NetworkTopNFlowQuery>
-);
+}: IPsQueryTabBodyProps) => {
+  const [
+    loading,
+    { id, inspect, isInspected, loadPage, networkTopNFlow, pageInfo, refetch, totalCount },
+  ] = useNetworkTopNFlow({
+    endDate,
+    flowTarget,
+    filterQuery,
+    skip,
+    startDate,
+    type: networkModel.NetworkType.page,
+  });
+
+  return (
+    <NetworkTopNFlowTableManage
+      data={networkTopNFlow}
+      fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
+      flowTargeted={flowTarget}
+      id={id}
+      inspect={inspect}
+      isInspect={isInspected}
+      loading={loading}
+      loadPage={loadPage}
+      refetch={refetch}
+      setQuery={setQuery}
+      showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
+      totalCount={totalCount}
+      type={networkModel.NetworkType.page}
+    />
+  );
+};
 
 IPsQueryTabBody.displayName = 'IPsQueryTabBody';
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts
index dee53a624baf..55d52d4ba325 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts
@@ -12,7 +12,6 @@ import {
   ResolverTree,
   ResolverEntityIndex,
 } from '../../../common/endpoint/types';
-import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../common/constants';
 
 /**
  * The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead.
@@ -38,13 +37,6 @@ export function dataAccessLayerFactory(
       });
     },
 
-    /**
-     * Used to get the default index pattern from the SIEM application.
-     */
-    indexPatterns(): string[] {
-      return context.services.uiSettings.get(defaultIndexKey);
-    },
-
     /**
      * Used to get the entity_id for an _id.
      */
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts
index 43282848dcf9..631eab18fc01 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts
@@ -12,7 +12,7 @@ import {
 import { mockTreeWithNoProcessEvents } from '../../mocks/resolver_tree';
 import { DataAccessLayer } from '../../types';
 
-type EmptiableRequests = 'relatedEvents' | 'resolverTree' | 'entities' | 'indexPatterns';
+type EmptiableRequests = 'relatedEvents' | 'resolverTree' | 'entities';
 
 interface Metadata<T> {
   /**
@@ -66,15 +66,6 @@ export function emptifyMock<T>(
           : dataAccessLayer.resolverTree(...args);
       },
 
-      /**
-       * Get an array of index patterns that contain events.
-       */
-      indexPatterns(...args): string[] {
-        return dataShouldBeEmpty.includes('indexPatterns')
-          ? []
-          : dataAccessLayer.indexPatterns(...args);
-      },
-
       /**
        * Get entities matching a document.
        */
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
index b0407fa5d7c1..0883a3787fcc 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
@@ -78,13 +78,6 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me
         );
       },
 
-      /**
-       * Get an array of index patterns that contain events.
-       */
-      indexPatterns(): string[] {
-        return ['index pattern'];
-      },
-
       /**
        * Get entities matching a document.
        */
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
new file mode 100644
index 000000000000..ec0fa9348578
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+  ResolverRelatedEvents,
+  ResolverTree,
+  ResolverEntityIndex,
+} from '../../../../common/endpoint/types';
+import { mockEndpointEvent } from '../../mocks/endpoint_event';
+import { mockTreeWithNoAncestorsAnd2Children } from '../../mocks/resolver_tree';
+import { DataAccessLayer } from '../../types';
+
+interface Metadata {
+  /**
+   * The `_id` of the document being analyzed.
+   */
+  databaseDocumentID: string;
+  /**
+   * A record of entityIDs to be used in tests assertions.
+   */
+  entityIDs: {
+    /**
+     * The entityID of the node related to the document being analyzed.
+     */
+    origin: 'origin';
+    /**
+     * The entityID of the first child of the origin.
+     */
+    firstChild: 'firstChild';
+    /**
+     * The entityID of the second child of the origin.
+     */
+    secondChild: 'secondChild';
+  };
+}
+
+/**
+ * A mock DataAccessLayer that will return an origin in two children. The `entity` response will be empty unless
+ * `awesome_index` is passed in the indices array.
+ */
+export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
+  dataAccessLayer: DataAccessLayer;
+  metadata: Metadata;
+} {
+  const metadata: Metadata = {
+    databaseDocumentID: '_id',
+    entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' },
+  };
+  return {
+    metadata,
+    dataAccessLayer: {
+      /**
+       * Fetch related events for an entity ID
+       */
+      relatedEvents(entityID: string): Promise<ResolverRelatedEvents> {
+        return Promise.resolve({
+          entityID,
+          events: [
+            mockEndpointEvent({
+              entityID,
+              name: 'event',
+              timestamp: 0,
+            }),
+          ],
+          nextEvent: null,
+        });
+      },
+
+      /**
+       * Fetch a ResolverTree for a entityID
+       */
+      resolverTree(): Promise<ResolverTree> {
+        return Promise.resolve(
+          mockTreeWithNoAncestorsAnd2Children({
+            originID: metadata.entityIDs.origin,
+            firstChildID: metadata.entityIDs.firstChild,
+            secondChildID: metadata.entityIDs.secondChild,
+          })
+        );
+      },
+
+      /**
+       * Get entities matching a document.
+       */
+      entities({ indices }): Promise<ResolverEntityIndex> {
+        // Only return values if the `indices` array contains exactly `'awesome_index'`
+        if (indices.length === 1 && indices[0] === 'awesome_index') {
+          return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]);
+        }
+        return Promise.resolve([]);
+      },
+    },
+  };
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
index 01e75e3eefdb..95ec0cd1a5f7 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
@@ -76,13 +76,6 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
         return Promise.resolve(tree);
       },
 
-      /**
-       * Get an array of index patterns that contain events.
-       */
-      indexPatterns(): string[] {
-        return ['index pattern'];
-      },
-
       /**
        * Get entities matching a document.
        */
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts
index baddcdfd0cd8..6a4955b104b8 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts
@@ -105,13 +105,6 @@ export function pausifyMock<T>({
         return dataAccessLayer.resolverTree(...args);
       },
 
-      /**
-       * Get an array of index patterns that contain events.
-       */
-      indexPatterns(...args): string[] {
-        return dataAccessLayer.indexPatterns(...args);
-      },
-
       /**
        * Get entities matching a document.
        */
diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts b/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts
new file mode 100644
index 000000000000..98efb459a069
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TreeFetcherParameters } from '../types';
+
+/**
+ * A factory for the most basic `TreeFetcherParameters`. Many tests need to provide this even when the values aren't relevant to the test.
+ */
+export function mockTreeFetcherParameters(): TreeFetcherParameters {
+  return {
+    databaseDocumentID: '',
+    indices: [],
+  };
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts
new file mode 100644
index 000000000000..faa4edfccdc3
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TreeFetcherParameters } from '../types';
+
+import { equal } from './tree_fetcher_parameters';
+describe('TreeFetcherParameters#equal:', () => {
+  const cases: Array<[TreeFetcherParameters, TreeFetcherParameters, boolean]> = [
+    // different databaseDocumentID
+    [{ databaseDocumentID: 'a', indices: [] }, { databaseDocumentID: 'b', indices: [] }, false],
+    // different indices length
+    [{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'a', indices: [] }, false],
+    // same indices length, different databaseDocumentID
+    [{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, false],
+    // 1 item in `indices`
+    [{ databaseDocumentID: 'b', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, true],
+    // 2 item in `indices`
+    [
+      { databaseDocumentID: 'b', indices: ['1', '2'] },
+      { databaseDocumentID: 'b', indices: ['1', '2'] },
+      true,
+    ],
+    // 2 item in `indices`, but order inversed
+    [
+      { databaseDocumentID: 'b', indices: ['2', '1'] },
+      { databaseDocumentID: 'b', indices: ['1', '2'] },
+      true,
+    ],
+  ];
+  describe.each(cases)('%p when compared to %p', (first, second, expected) => {
+    it(`should ${expected ? '' : 'not'}be equal`, () => {
+      expect(equal(first, second)).toBe(expected);
+    });
+  });
+});
diff --git a/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts
new file mode 100644
index 000000000000..d8280c749090
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TreeFetcherParameters } from '../types';
+
+/**
+ * Determine if two instances of `TreeFetcherParameters` are equivalent. Use this to determine if
+ * a change to a `TreeFetcherParameters` warrants invaliding a request or response.
+ */
+export function equal(param1: TreeFetcherParameters, param2?: TreeFetcherParameters): boolean {
+  if (!param2) {
+    return false;
+  }
+  if (param1 === param2) {
+    return true;
+  }
+  if (param1.databaseDocumentID !== param2.databaseDocumentID) {
+    return false;
+  }
+  return arraysContainTheSameElements(param1.indices, param2.indices);
+}
+
+function arraysContainTheSameElements(first: unknown[], second: unknown[]): boolean {
+  if (first === second) {
+    return true;
+  }
+  if (first.length !== second.length) {
+    return false;
+  }
+  const firstSet = new Set(first);
+  for (let index = 0; index < second.length; index++) {
+    if (!firstSet.has(second[index])) {
+      return false;
+    }
+  }
+  return true;
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts
index e03f24d78e2a..7d71cbd97b9e 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts
@@ -115,7 +115,7 @@ interface AppReceivedNewExternalProperties {
     /**
      * the `_id` of an ES document. This defines the origin of the Resolver graph.
      */
-    databaseDocumentID?: string;
+    databaseDocumentID: string;
     /**
      * An ID that uniquely identifies this Resolver instance from other concurrent Resolvers.
      */
@@ -125,6 +125,11 @@ interface AppReceivedNewExternalProperties {
      * The `search` part of the URL of this page.
      */
     locationSearch: string;
+
+    /**
+     * Indices that the backend will use to find the document.
+     */
+    indices: string[];
   };
 }
 
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts
index 466c37d4ad5f..59d1494ae8c2 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts
@@ -5,6 +5,7 @@
  */
 
 import { ResolverRelatedEvents, ResolverTree } from '../../../../common/endpoint/types';
+import { TreeFetcherParameters } from '../../types';
 
 interface ServerReturnedResolverData {
   readonly type: 'serverReturnedResolverData';
@@ -14,9 +15,9 @@ interface ServerReturnedResolverData {
      */
     result: ResolverTree;
     /**
-     * The database document ID that was used to fetch the resolver tree
+     * The database parameters that was used to fetch the resolver tree
      */
-    databaseDocumentID: string;
+    parameters: TreeFetcherParameters;
   };
 }
 
@@ -25,7 +26,7 @@ interface AppRequestedResolverData {
   /**
    * entity ID used to make the request.
    */
-  readonly payload: string;
+  readonly payload: TreeFetcherParameters;
 }
 
 interface ServerFailedToReturnResolverData {
@@ -33,7 +34,7 @@ interface ServerFailedToReturnResolverData {
   /**
    * entity ID used to make the failed request
    */
-  readonly payload: string;
+  readonly payload: TreeFetcherParameters;
 }
 
 interface AppAbortedResolverDataRequest {
@@ -41,7 +42,7 @@ interface AppAbortedResolverDataRequest {
   /**
    * entity ID used to make the aborted request
    */
-  readonly payload: string;
+  readonly payload: TreeFetcherParameters;
 }
 
 /**
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts
index 21c4f92f8e50..e6e525334e81 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts
@@ -12,6 +12,7 @@ import { DataState } from '../../types';
 import { DataAction } from './action';
 import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types';
 import * as eventModel from '../../../../common/endpoint/models/event';
+import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters';
 
 /**
  * Test the data reducer and selector.
@@ -27,7 +28,7 @@ describe('Resolver Data Middleware', () => {
         type: 'serverReturnedResolverData',
         payload: {
           result: tree,
-          databaseDocumentID: '',
+          parameters: mockTreeFetcherParameters(),
         },
       };
       store.dispatch(action);
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts
index c43182ddbf83..c8df95aaee6f 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts
@@ -7,34 +7,53 @@
 import { Reducer } from 'redux';
 import { DataState } from '../../types';
 import { ResolverAction } from '../actions';
+import * as treeFetcherParameters from '../../models/tree_fetcher_parameters';
 
 const initialState: DataState = {
   relatedEvents: new Map(),
   relatedEventsReady: new Map(),
   resolverComponentInstanceID: undefined,
+  tree: {},
 };
 
 export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState, action) => {
   if (action.type === 'appReceivedNewExternalProperties') {
     const nextState: DataState = {
       ...state,
-      databaseDocumentID: action.payload.databaseDocumentID,
+      tree: {
+        ...state.tree,
+        currentParameters: {
+          databaseDocumentID: action.payload.databaseDocumentID,
+          indices: action.payload.indices,
+        },
+      },
       resolverComponentInstanceID: action.payload.resolverComponentInstanceID,
     };
     return nextState;
   } else if (action.type === 'appRequestedResolverData') {
     // keep track of what we're requesting, this way we know when to request and when not to.
-    return {
+    const nextState: DataState = {
       ...state,
-      pendingRequestDatabaseDocumentID: action.payload,
+      tree: {
+        ...state.tree,
+        pendingRequestParameters: {
+          databaseDocumentID: action.payload.databaseDocumentID,
+          indices: action.payload.indices,
+        },
+      },
     };
+    return nextState;
   } else if (action.type === 'appAbortedResolverDataRequest') {
-    if (action.payload === state.pendingRequestDatabaseDocumentID) {
+    if (treeFetcherParameters.equal(action.payload, state.tree.pendingRequestParameters)) {
       // the request we were awaiting was aborted
-      return {
+      const nextState: DataState = {
         ...state,
-        pendingRequestDatabaseDocumentID: undefined,
+        tree: {
+          ...state.tree,
+          pendingRequestParameters: undefined,
+        },
       };
+      return nextState;
     } else {
       return state;
     }
@@ -43,29 +62,35 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
     const nextState: DataState = {
       ...state,
 
-      /**
-       * Store the last received data, as well as the databaseDocumentID it relates to.
-       */
-      lastResponse: {
-        result: action.payload.result,
-        databaseDocumentID: action.payload.databaseDocumentID,
-        successful: true,
-      },
+      tree: {
+        ...state.tree,
+        /**
+         * Store the last received data, as well as the databaseDocumentID it relates to.
+         */
+        lastResponse: {
+          result: action.payload.result,
+          parameters: action.payload.parameters,
+          successful: true,
+        },
 
-      // This assumes that if we just received something, there is no longer a pending request.
-      // This cannot model multiple in-flight requests
-      pendingRequestDatabaseDocumentID: undefined,
+        // This assumes that if we just received something, there is no longer a pending request.
+        // This cannot model multiple in-flight requests
+        pendingRequestParameters: undefined,
+      },
     };
     return nextState;
   } else if (action.type === 'serverFailedToReturnResolverData') {
     /** Only handle this if we are expecting a response */
-    if (state.pendingRequestDatabaseDocumentID !== undefined) {
+    if (state.tree.pendingRequestParameters !== undefined) {
       const nextState: DataState = {
         ...state,
-        pendingRequestDatabaseDocumentID: undefined,
-        lastResponse: {
-          databaseDocumentID: state.pendingRequestDatabaseDocumentID,
-          successful: false,
+        tree: {
+          ...state.tree,
+          pendingRequestParameters: undefined,
+          lastResponse: {
+            parameters: state.tree.pendingRequestParameters,
+            successful: false,
+          },
         },
       };
       return nextState;
@@ -76,16 +101,18 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
     action.type === 'userRequestedRelatedEventData' ||
     action.type === 'appDetectedMissingEventData'
   ) {
-    return {
+    const nextState: DataState = {
       ...state,
       relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload, false]]),
     };
+    return nextState;
   } else if (action.type === 'serverReturnedRelatedEventData') {
-    return {
+    const nextState: DataState = {
       ...state,
       relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload.entityID, true]]),
       relatedEvents: new Map([...state.relatedEvents, [action.payload.entityID, action.payload]]),
     };
+    return nextState;
   } else {
     return state;
   }
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts
index dc478ede7279..539325faffdf 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts
@@ -18,6 +18,7 @@ import {
 } from '../../mocks/resolver_tree';
 import { uniquePidForProcess } from '../../models/process_event';
 import { EndpointEvent } from '../../../../common/endpoint/types';
+import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters';
 
 describe('data state', () => {
   let actions: ResolverAction[] = [];
@@ -39,29 +40,32 @@ describe('data state', () => {
    */
   const viewAsAString = (dataState: DataState) => {
     return [
-      ['is loading', selectors.isLoading(dataState)],
-      ['has an error', selectors.hasError(dataState)],
+      ['is loading', selectors.isTreeLoading(dataState)],
+      ['has an error', selectors.hadErrorLoadingTree(dataState)],
       ['has more children', selectors.hasMoreChildren(dataState)],
       ['has more ancestors', selectors.hasMoreAncestors(dataState)],
-      ['document to fetch', selectors.databaseDocumentIDToFetch(dataState)],
-      ['requires a pending request to be aborted', selectors.databaseDocumentIDToAbort(dataState)],
+      ['parameters to fetch', selectors.treeParametersToFetch(dataState)],
+      [
+        'requires a pending request to be aborted',
+        selectors.treeRequestParametersToAbort(dataState),
+      ],
     ]
       .map(([message, value]) => `${message}: ${JSON.stringify(value)}`)
       .join('\n');
   };
 
-  it(`shouldn't initially be loading, or have an error, or have more children or ancestors, or have a document to fetch, or have a pending request that needs to be aborted.`, () => {
+  it(`shouldn't initially be loading, or have an error, or have more children or ancestors, or have a request to make, or have a pending request that needs to be aborted.`, () => {
     expect(viewAsAString(state())).toMatchInlineSnapshot(`
       "is loading: false
       has an error: false
       has more children: false
       has more ancestors: false
-      document to fetch: null
+      parameters to fetch: null
       requires a pending request to be aborted: null"
     `);
   });
 
-  describe('when there is a databaseDocumentID but no pending request', () => {
+  describe('when there are parameters to fetch but no pending request', () => {
     const databaseDocumentID = 'databaseDocumentID';
     const resolverComponentInstanceID = 'resolverComponentInstanceID';
     beforeEach(() => {
@@ -74,12 +78,13 @@ describe('data state', () => {
 
             // `locationSearch` doesn't matter for this test
             locationSearch: '',
+            indices: [],
           },
         },
       ];
     });
-    it('should need to fetch the databaseDocumentID', () => {
-      expect(selectors.databaseDocumentIDToFetch(state())).toBe(databaseDocumentID);
+    it('should need to request the tree', () => {
+      expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe(databaseDocumentID);
     });
     it('should not be loading, have an error, have more children or ancestors, or have a pending request that needs to be aborted.', () => {
       expect(viewAsAString(state())).toMatchInlineSnapshot(`
@@ -87,39 +92,41 @@ describe('data state', () => {
         has an error: false
         has more children: false
         has more ancestors: false
-        document to fetch: \\"databaseDocumentID\\"
+        parameters to fetch: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[]}
         requires a pending request to be aborted: null"
       `);
     });
   });
-  describe('when there is a pending request but no databaseDocumentID', () => {
+  describe('when there is a pending request but no current tree fetching parameters', () => {
     const databaseDocumentID = 'databaseDocumentID';
     beforeEach(() => {
       actions = [
         {
           type: 'appRequestedResolverData',
-          payload: databaseDocumentID,
+          payload: { databaseDocumentID, indices: [] },
         },
       ];
     });
     it('should be loading', () => {
-      expect(selectors.isLoading(state())).toBe(true);
+      expect(selectors.isTreeLoading(state())).toBe(true);
     });
     it('should have a request to abort', () => {
-      expect(selectors.databaseDocumentIDToAbort(state())).toBe(databaseDocumentID);
+      expect(selectors.treeRequestParametersToAbort(state())?.databaseDocumentID).toBe(
+        databaseDocumentID
+      );
     });
-    it('should not have an error, more children, more ancestors, or a document to fetch.', () => {
+    it('should not have an error, more children, more ancestors, or request to make.', () => {
       expect(viewAsAString(state())).toMatchInlineSnapshot(`
         "is loading: true
         has an error: false
         has more children: false
         has more ancestors: false
-        document to fetch: null
-        requires a pending request to be aborted: \\"databaseDocumentID\\""
+        parameters to fetch: null
+        requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[]}"
       `);
     });
   });
-  describe('when there is a pending request for the current databaseDocumentID', () => {
+  describe('when there is a pending request that was made using the current parameters', () => {
     const databaseDocumentID = 'databaseDocumentID';
     const resolverComponentInstanceID = 'resolverComponentInstanceID';
     beforeEach(() => {
@@ -132,27 +139,28 @@ describe('data state', () => {
 
             // `locationSearch` doesn't matter for this test
             locationSearch: '',
+            indices: [],
           },
         },
         {
           type: 'appRequestedResolverData',
-          payload: databaseDocumentID,
+          payload: { databaseDocumentID, indices: [] },
         },
       ];
     });
     it('should be loading', () => {
-      expect(selectors.isLoading(state())).toBe(true);
+      expect(selectors.isTreeLoading(state())).toBe(true);
     });
     it('should not have a request to abort', () => {
-      expect(selectors.databaseDocumentIDToAbort(state())).toBe(null);
+      expect(selectors.treeRequestParametersToAbort(state())).toBe(null);
     });
-    it('should not have an error, more children, more ancestors, a document to begin fetching, or a pending request that should be aborted.', () => {
+    it('should not have an error, more children, more ancestors, a request to make, or a pending request that should be aborted.', () => {
       expect(viewAsAString(state())).toMatchInlineSnapshot(`
         "is loading: true
         has an error: false
         has more children: false
         has more ancestors: false
-        document to fetch: null
+        parameters to fetch: null
         requires a pending request to be aborted: null"
       `);
     });
@@ -160,28 +168,28 @@ describe('data state', () => {
       beforeEach(() => {
         actions.push({
           type: 'serverFailedToReturnResolverData',
-          payload: databaseDocumentID,
+          payload: { databaseDocumentID, indices: [] },
         });
       });
       it('should not be loading', () => {
-        expect(selectors.isLoading(state())).toBe(false);
+        expect(selectors.isTreeLoading(state())).toBe(false);
       });
       it('should have an error', () => {
-        expect(selectors.hasError(state())).toBe(true);
+        expect(selectors.hadErrorLoadingTree(state())).toBe(true);
       });
-      it('should not be loading, have more children, have more ancestors, have a document to fetch, or have a pending request that needs to be aborted.', () => {
+      it('should not be loading, have more children, have more ancestors, have a request to make, or have a pending request that needs to be aborted.', () => {
         expect(viewAsAString(state())).toMatchInlineSnapshot(`
           "is loading: false
           has an error: true
           has more children: false
           has more ancestors: false
-          document to fetch: null
+          parameters to fetch: null
           requires a pending request to be aborted: null"
         `);
       });
     });
   });
-  describe('when there is a pending request for a different databaseDocumentID than the current one', () => {
+  describe('when there is a pending request that was made with parameters that are different than the current tree fetching parameters', () => {
     const firstDatabaseDocumentID = 'first databaseDocumentID';
     const secondDatabaseDocumentID = 'second databaseDocumentID';
     const resolverComponentInstanceID1 = 'resolverComponentInstanceID1';
@@ -196,12 +204,13 @@ describe('data state', () => {
             resolverComponentInstanceID: resolverComponentInstanceID1,
             // `locationSearch` doesn't matter for this test
             locationSearch: '',
+            indices: [],
           },
         },
         // this happens when the middleware starts the request
         {
           type: 'appRequestedResolverData',
-          payload: firstDatabaseDocumentID,
+          payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] },
         },
         // receive a different databaseDocumentID. this should cause the middleware to abort the existing request and start a new one
         {
@@ -211,18 +220,23 @@ describe('data state', () => {
             resolverComponentInstanceID: resolverComponentInstanceID2,
             // `locationSearch` doesn't matter for this test
             locationSearch: '',
+            indices: [],
           },
         },
       ];
     });
     it('should be loading', () => {
-      expect(selectors.isLoading(state())).toBe(true);
+      expect(selectors.isTreeLoading(state())).toBe(true);
     });
-    it('should need to fetch the second databaseDocumentID', () => {
-      expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID);
+    it('should need to request the tree using the second set of parameters', () => {
+      expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe(
+        secondDatabaseDocumentID
+      );
     });
     it('should need to abort the request for the databaseDocumentID', () => {
-      expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID);
+      expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe(
+        secondDatabaseDocumentID
+      );
     });
     it('should use the correct location for the second resolver', () => {
       expect(selectors.resolverComponentInstanceID(state())).toBe(resolverComponentInstanceID2);
@@ -233,25 +247,27 @@ describe('data state', () => {
         has an error: false
         has more children: false
         has more ancestors: false
-        document to fetch: \\"second databaseDocumentID\\"
-        requires a pending request to be aborted: \\"first databaseDocumentID\\""
+        parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[]}
+        requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"first databaseDocumentID\\",\\"indices\\":[]}"
       `);
     });
     describe('and when the old request was aborted', () => {
       beforeEach(() => {
         actions.push({
           type: 'appAbortedResolverDataRequest',
-          payload: firstDatabaseDocumentID,
+          payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] },
         });
       });
       it('should not require a pending request to be aborted', () => {
-        expect(selectors.databaseDocumentIDToAbort(state())).toBe(null);
+        expect(selectors.treeRequestParametersToAbort(state())).toBe(null);
       });
       it('should have a document to fetch', () => {
-        expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID);
+        expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe(
+          secondDatabaseDocumentID
+        );
       });
       it('should not be loading', () => {
-        expect(selectors.isLoading(state())).toBe(false);
+        expect(selectors.isTreeLoading(state())).toBe(false);
       });
       it('should not have an error, more children, or more ancestors.', () => {
         expect(viewAsAString(state())).toMatchInlineSnapshot(`
@@ -259,7 +275,7 @@ describe('data state', () => {
           has an error: false
           has more children: false
           has more ancestors: false
-          document to fetch: \\"second databaseDocumentID\\"
+          parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[]}
           requires a pending request to be aborted: null"
         `);
       });
@@ -267,14 +283,14 @@ describe('data state', () => {
         beforeEach(() => {
           actions.push({
             type: 'appRequestedResolverData',
-            payload: secondDatabaseDocumentID,
+            payload: { databaseDocumentID: secondDatabaseDocumentID, indices: [] },
           });
         });
         it('should not have a document ID to fetch', () => {
-          expect(selectors.databaseDocumentIDToFetch(state())).toBe(null);
+          expect(selectors.treeParametersToFetch(state())).toBe(null);
         });
         it('should be loading', () => {
-          expect(selectors.isLoading(state())).toBe(true);
+          expect(selectors.isTreeLoading(state())).toBe(true);
         });
         it('should not have an error, more children, more ancestors, or a pending request that needs to be aborted.', () => {
           expect(viewAsAString(state())).toMatchInlineSnapshot(`
@@ -282,7 +298,7 @@ describe('data state', () => {
             has an error: false
             has more children: false
             has more ancestors: false
-            document to fetch: null
+            parameters to fetch: null
             requires a pending request to be aborted: null"
           `);
         });
@@ -303,7 +319,7 @@ describe('data state', () => {
             secondAncestorID,
           }),
           // this value doesn't matter
-          databaseDocumentID: '',
+          parameters: mockTreeFetcherParameters(),
         },
       });
     });
@@ -331,7 +347,7 @@ describe('data state', () => {
             secondAncestorID,
           }),
           // this value doesn't matter
-          databaseDocumentID: '',
+          parameters: mockTreeFetcherParameters(),
         },
       });
     });
@@ -355,7 +371,7 @@ describe('data state', () => {
         payload: {
           result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }),
           // this value doesn't matter
-          databaseDocumentID: '',
+          parameters: mockTreeFetcherParameters(),
         },
       });
     });
@@ -386,7 +402,7 @@ describe('data state', () => {
         payload: {
           result: tree,
           // this value doesn't matter
-          databaseDocumentID: '',
+          parameters: mockTreeFetcherParameters(),
         },
       });
     });
@@ -417,7 +433,7 @@ describe('data state', () => {
         payload: {
           result: tree,
           // this value doesn't matter
-          databaseDocumentID: '',
+          parameters: mockTreeFetcherParameters(),
         },
       });
     });
@@ -433,7 +449,7 @@ describe('data state', () => {
         payload: {
           result: tree,
           // this value doesn't matter
-          databaseDocumentID: '',
+          parameters: mockTreeFetcherParameters(),
         },
       });
     });
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
index eaa80b46471f..e647828ddb60 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
@@ -15,6 +15,7 @@ import {
   AABB,
   VisibleEntites,
   SectionData,
+  TreeFetcherParameters,
 } from '../../types';
 import {
   isGraphableProcess,
@@ -34,6 +35,7 @@ import {
   LegacyEndpointEvent,
 } from '../../../../common/endpoint/types';
 import * as resolverTreeModel from '../../models/resolver_tree';
+import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters';
 import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout';
 import * as eventModel from '../../../../common/endpoint/models/event';
 import * as vector2 from '../../models/vector2';
@@ -42,26 +44,25 @@ import { formatDate } from '../../view/panels/panel_content_utilities';
 /**
  * If there is currently a request.
  */
-export function isLoading(state: DataState): boolean {
-  return state.pendingRequestDatabaseDocumentID !== undefined;
+export function isTreeLoading(state: DataState): boolean {
+  return state.tree.pendingRequestParameters !== undefined;
 }
 
 /**
- * A string for uniquely identifying the instance of resolver within the app.
+ * If a request was made and it threw an error or returned a failure response code.
  */
-export function resolverComponentInstanceID(state: DataState): string {
-  return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : '';
+export function hadErrorLoadingTree(state: DataState): boolean {
+  if (state.tree.lastResponse) {
+    return !state.tree.lastResponse.successful;
+  }
+  return false;
 }
 
 /**
- * If a request was made and it threw an error or returned a failure response code.
+ * A string for uniquely identifying the instance of resolver within the app.
  */
-export function hasError(state: DataState): boolean {
-  if (state.lastResponse && state.lastResponse.successful === false) {
-    return true;
-  } else {
-    return false;
-  }
+export function resolverComponentInstanceID(state: DataState): string {
+  return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : '';
 }
 
 /**
@@ -69,11 +70,7 @@ export function hasError(state: DataState): boolean {
  * we're currently interested in.
  */
 const resolverTreeResponse = (state: DataState): ResolverTree | undefined => {
-  if (state.lastResponse && state.lastResponse.successful) {
-    return state.lastResponse.result;
-  } else {
-    return undefined;
-  }
+  return state.tree.lastResponse?.successful ? state.tree.lastResponse.result : undefined;
 };
 
 /**
@@ -458,18 +455,24 @@ export const relatedEventInfoByEntityId: (
 );
 
 /**
- * If we need to fetch, this is the ID to fetch.
+ * If the tree resource needs to be fetched then these are the parameters that should be used.
  */
-export function databaseDocumentIDToFetch(state: DataState): string | null {
-  // If there is an ID, it must match either the last received version, or the pending version.
-  // Otherwise, we need to fetch it
-  // NB: this technique will not allow for refreshing of data.
+export function treeParametersToFetch(state: DataState): TreeFetcherParameters | null {
+  /**
+   * If there are current tree parameters that don't match the parameters used in the pending request (if there is a pending request) and that don't match the parameters used in the last completed request (if there was a last completed request) then we need to fetch the tree resource using the current parameters.
+   */
   if (
-    state.databaseDocumentID !== undefined &&
-    state.databaseDocumentID !== state.pendingRequestDatabaseDocumentID &&
-    state.databaseDocumentID !== state.lastResponse?.databaseDocumentID
+    state.tree.currentParameters !== undefined &&
+    !treeFetcherParametersModel.equal(
+      state.tree.currentParameters,
+      state.tree.lastResponse?.parameters
+    ) &&
+    !treeFetcherParametersModel.equal(
+      state.tree.currentParameters,
+      state.tree.pendingRequestParameters
+    )
   ) {
-    return state.databaseDocumentID;
+    return state.tree.currentParameters;
   } else {
     return null;
   }
@@ -692,15 +695,18 @@ export const nodesAndEdgelines: (
 /**
  * If there is a pending request that's for a entity ID that doesn't matche the `entityID`, then we should cancel it.
  */
-export function databaseDocumentIDToAbort(state: DataState): string | null {
+export function treeRequestParametersToAbort(state: DataState): TreeFetcherParameters | null {
   /**
-   * If there is a pending request, and its not for the current databaseDocumentID (even, if the current databaseDocumentID is undefined) then we should abort the request.
+   * If there is a pending request, and its not for the current parameters (even, if the current parameters are undefined) then we should abort the request.
    */
   if (
-    state.pendingRequestDatabaseDocumentID !== undefined &&
-    state.pendingRequestDatabaseDocumentID !== state.databaseDocumentID
+    state.tree.pendingRequestParameters !== undefined &&
+    !treeFetcherParametersModel.equal(
+      state.tree.pendingRequestParameters,
+      state.tree.currentParameters
+    )
   ) {
-    return state.pendingRequestDatabaseDocumentID;
+    return state.tree.pendingRequestParameters;
   } else {
     return null;
   }
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts
index e91c455c9445..28948debae89 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts
@@ -12,6 +12,7 @@ import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/
 import { visibleNodesAndEdgeLines } from '../selectors';
 import { mockProcessEvent } from '../../models/process_event_test_helpers';
 import { mock as mockResolverTree } from '../../models/resolver_tree';
+import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters';
 
 describe('resolver visible entities', () => {
   let processA: LegacyEndpointEvent;
@@ -112,7 +113,7 @@ describe('resolver visible entities', () => {
       ];
       const action: ResolverAction = {
         type: 'serverReturnedResolverData',
-        payload: { result: mockResolverTree({ events })!, databaseDocumentID: '' },
+        payload: { result: mockResolverTree({ events })!, parameters: mockTreeFetcherParameters() },
       };
       const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [300, 200] };
       store.dispatch(action);
@@ -140,7 +141,7 @@ describe('resolver visible entities', () => {
       ];
       const action: ResolverAction = {
         type: 'serverReturnedResolverData',
-        payload: { result: mockResolverTree({ events })!, databaseDocumentID: '' },
+        payload: { result: mockResolverTree({ events })!, parameters: mockTreeFetcherParameters() },
       };
       const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [2000, 2000] };
       store.dispatch(action);
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts
index 0ec340efbdac..ef4ca2380ebf 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts
@@ -28,32 +28,31 @@ export function ResolverTreeFetcher(
   // if the entityID changes while
   return async () => {
     const state = api.getState();
-    const databaseDocumentIDToFetch = selectors.databaseDocumentIDToFetch(state);
+    const databaseParameters = selectors.treeParametersToFetch(state);
 
-    if (selectors.databaseDocumentIDToAbort(state) && lastRequestAbortController) {
+    if (selectors.treeRequestParametersToAbort(state) && lastRequestAbortController) {
       lastRequestAbortController.abort();
       // calling abort will cause an action to be fired
-    } else if (databaseDocumentIDToFetch !== null) {
+    } else if (databaseParameters !== null) {
       lastRequestAbortController = new AbortController();
       let result: ResolverTree | undefined;
       // Inform the state that we've made the request. Without this, the middleware will try to make the request again
       // immediately.
       api.dispatch({
         type: 'appRequestedResolverData',
-        payload: databaseDocumentIDToFetch,
+        payload: databaseParameters,
       });
       try {
-        const indices: string[] = dataAccessLayer.indexPatterns();
         const matchingEntities: ResolverEntityIndex = await dataAccessLayer.entities({
-          _id: databaseDocumentIDToFetch,
-          indices,
+          _id: databaseParameters.databaseDocumentID,
+          indices: databaseParameters.indices ?? [],
           signal: lastRequestAbortController.signal,
         });
         if (matchingEntities.length < 1) {
           // If no entity_id could be found for the _id, bail out with a failure.
           api.dispatch({
             type: 'serverFailedToReturnResolverData',
-            payload: databaseDocumentIDToFetch,
+            payload: databaseParameters,
           });
           return;
         }
@@ -67,12 +66,12 @@ export function ResolverTreeFetcher(
         if (error instanceof DOMException && error.name === 'AbortError') {
           api.dispatch({
             type: 'appAbortedResolverDataRequest',
-            payload: databaseDocumentIDToFetch,
+            payload: databaseParameters,
           });
         } else {
           api.dispatch({
             type: 'serverFailedToReturnResolverData',
-            payload: databaseDocumentIDToFetch,
+            payload: databaseParameters,
           });
         }
       }
@@ -81,7 +80,7 @@ export function ResolverTreeFetcher(
           type: 'serverReturnedResolverData',
           payload: {
             result,
-            databaseDocumentID: databaseDocumentIDToFetch,
+            parameters: databaseParameters,
           },
         });
       }
diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts
index f113e861d3ce..d15274f0363a 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts
@@ -14,6 +14,7 @@ import {
   mockTreeWithNoAncestorsAnd2Children,
 } from '../mocks/resolver_tree';
 import { SafeResolverEvent } from '../../../common/endpoint/types';
+import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters';
 
 describe('resolver selectors', () => {
   const actions: ResolverAction[] = [];
@@ -43,7 +44,7 @@ describe('resolver selectors', () => {
               secondAncestorID,
             }),
             // this value doesn't matter
-            databaseDocumentID: '',
+            parameters: mockTreeFetcherParameters(),
           },
         });
       });
@@ -77,7 +78,7 @@ describe('resolver selectors', () => {
           payload: {
             result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }),
             // this value doesn't matter
-            databaseDocumentID: '',
+            parameters: mockTreeFetcherParameters(),
           },
         });
       });
diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
index bdea08df3d7f..8ea0bc9199cb 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
@@ -84,14 +84,14 @@ export const layout: (state: ResolverState) => IsometricTaxiLayout = composeSele
 /**
  * If we need to fetch, this is the entity ID to fetch.
  */
-export const databaseDocumentIDToFetch = composeSelectors(
+export const treeParametersToFetch = composeSelectors(
   dataStateSelector,
-  dataSelectors.databaseDocumentIDToFetch
+  dataSelectors.treeParametersToFetch
 );
 
-export const databaseDocumentIDToAbort = composeSelectors(
+export const treeRequestParametersToAbort = composeSelectors(
   dataStateSelector,
-  dataSelectors.databaseDocumentIDToAbort
+  dataSelectors.treeRequestParametersToAbort
 );
 
 export const resolverComponentInstanceID = composeSelectors(
@@ -207,12 +207,15 @@ function uiStateSelector(state: ResolverState) {
 /**
  * Whether or not the resolver is pending fetching data
  */
-export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoading);
+export const isTreeLoading = composeSelectors(dataStateSelector, dataSelectors.isTreeLoading);
 
 /**
  * Whether or not the resolver encountered an error while fetching data
  */
-export const hasError = composeSelectors(dataStateSelector, dataSelectors.hasError);
+export const hadErrorLoadingTree = composeSelectors(
+  dataStateSelector,
+  dataSelectors.hadErrorLoadingTree
+);
 
 /**
  * True if the children cursor is not null
diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx
index a6520c8f0e06..9d10d1c2b64a 100644
--- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx
@@ -49,6 +49,7 @@ export class Simulator {
     dataAccessLayer,
     resolverComponentInstanceID,
     databaseDocumentID,
+    indices,
     history,
   }: {
     /**
@@ -59,10 +60,14 @@ export class Simulator {
      * A string that uniquely identifies this Resolver instance among others mounted in the DOM.
      */
     resolverComponentInstanceID: string;
+    /**
+     * Indices that the backend would use to find the document ID.
+     */
+    indices: string[];
     /**
      * a databaseDocumentID to pass to Resolver. Resolver will use this in requests to the mock data layer.
      */
-    databaseDocumentID?: string;
+    databaseDocumentID: string;
     history?: HistoryPackageHistoryInterface<never>;
   }) {
     // create the spy middleware (for debugging tests)
@@ -99,6 +104,7 @@ export class Simulator {
         store={this.store}
         coreStart={coreStart}
         databaseDocumentID={databaseDocumentID}
+        indices={indices}
       />
     );
   }
@@ -124,6 +130,20 @@ export class Simulator {
     this.wrapper.setProps({ resolverComponentInstanceID: value });
   }
 
+  /**
+   * Change the indices (updates the React component props.)
+   */
+  public set indices(value: string[]) {
+    this.wrapper.setProps({ indices: value });
+  }
+
+  /**
+   * Get the indices (updates the React component props.)
+   */
+  public get indices(): string[] {
+    return this.wrapper.prop('indices');
+  }
+
   /**
    * Call this to console.log actions (and state). Use this to debug your tests.
    * State and actions aren't exposed otherwise because the tests using this simulator should
diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx
index 5d5a414761db..89218e9fca8c 100644
--- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx
@@ -99,6 +99,7 @@ export const MockResolver = React.memo((props: MockResolverProps) => {
                 ref={resolverRef}
                 databaseDocumentID={props.databaseDocumentID}
                 resolverComponentInstanceID={props.resolverComponentInstanceID}
+                indices={props.indices}
               />
             </Provider>
           </SideEffectContext.Provider>
diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts
index e8304bf838e2..1e7e2a8eba8a 100644
--- a/x-pack/plugins/security_solution/public/resolver/types.ts
+++ b/x-pack/plugins/security_solution/public/resolver/types.ts
@@ -194,53 +194,68 @@ export interface VisibleEntites {
   connectingEdgeLineSegments: EdgeLineSegment[];
 }
 
+export interface TreeFetcherParameters {
+  /**
+   * The `_id` for an ES document. Used to select a process that we'll show the graph for.
+   */
+  databaseDocumentID: string;
+
+  /**
+   * The indices that the backend will use to search for the document ID.
+   */
+  indices: string[];
+}
+
 /**
  * State for `data` reducer which handles receiving Resolver data from the back-end.
  */
 export interface DataState {
   readonly relatedEvents: Map<string, ResolverRelatedEvents>;
   readonly relatedEventsReady: Map<string, boolean>;
-  /**
-   * The `_id` for an ES document. Used to select a process that we'll show the graph for.
-   */
-  readonly databaseDocumentID?: string;
-  /**
-   * The id used for the pending request, if there is one.
-   */
-  readonly pendingRequestDatabaseDocumentID?: string;
+
+  readonly tree: {
+    /**
+     * The parameters passed from the resolver properties
+     */
+    readonly currentParameters?: TreeFetcherParameters;
+
+    /**
+     * The id used for the pending request, if there is one.
+     */
+    readonly pendingRequestParameters?: TreeFetcherParameters;
+    /**
+     * The parameters and response from the last successful request.
+     */
+    readonly lastResponse?: {
+      /**
+       * The id used in the request.
+       */
+      readonly parameters: TreeFetcherParameters;
+    } & (
+      | {
+          /**
+           * If a response with a success code was received, this is `true`.
+           */
+          readonly successful: true;
+          /**
+           * The ResolverTree parsed from the response.
+           */
+          readonly result: ResolverTree;
+        }
+      | {
+          /**
+           * If the request threw an exception or the response had a failure code, this will be false.
+           */
+          readonly successful: false;
+        }
+    );
+  };
 
   /**
    * An ID that is used to differentiate this Resolver instance from others concurrently running on the same page.
    * Used to prevent collisions in things like query parameters.
    */
   readonly resolverComponentInstanceID?: string;
-
-  /**
-   * The parameters and response from the last successful request.
-   */
-  readonly lastResponse?: {
-    /**
-     * The id used in the request.
-     */
-    readonly databaseDocumentID: string;
-  } & (
-    | {
-        /**
-         * If a response with a success code was received, this is `true`.
-         */
-        readonly successful: true;
-        /**
-         * The ResolverTree parsed from the response.
-         */
-        readonly result: ResolverTree;
-      }
-    | {
-        /**
-         * If the request threw an exception or the response had a failure code, this will be false.
-         */
-        readonly successful: false;
-      }
-  );
 }
 
 /**
@@ -494,11 +509,6 @@ export interface DataAccessLayer {
    */
   resolverTree: (entityID: string, signal: AbortSignal) => Promise<ResolverTree>;
 
-  /**
-   * Get an array of index patterns that contain events.
-   */
-  indexPatterns: () => string[];
-
   /**
    * Get entities matching a document.
    */
@@ -524,13 +534,18 @@ export interface ResolverProps {
    * The `_id` value of an event in ES.
    * Used as the origin of the Resolver graph.
    */
-  databaseDocumentID?: string;
+  databaseDocumentID: string;
 
   /**
    * An ID that is used to differentiate this Resolver instance from others concurrently running on the same page.
    * Used to prevent collisions in things like query parameters.
    */
   resolverComponentInstanceID: string;
+
+  /**
+   * Indices that the backend should use to find the originating document.
+   */
+  indices: string[];
 }
 
 /**
diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
index 1e5ac093cac7..223ce728f426 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
@@ -4,6 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
+import { noAncestorsTwoChildenInIndexCalledAwesomeIndex } from '../data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index';
 import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children';
 import { Simulator } from '../test_utilities/simulator';
 // Extend jest with a custom matcher
@@ -19,6 +20,62 @@ let entityIDs: { origin: string; firstChild: string; secondChild: string };
 // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances
 const resolverComponentInstanceID = 'resolverComponentInstanceID';
 
+describe("Resolver, when rendered with the `indices` prop set to `[]` and the `databaseDocumentID` prop set to `_id`, and when the document is found in an index called 'awesome_index'", () => {
+  beforeEach(async () => {
+    // create a mock data access layer
+    const {
+      metadata: dataAccessLayerMetadata,
+      dataAccessLayer,
+    } = noAncestorsTwoChildenInIndexCalledAwesomeIndex();
+
+    // save a reference to the entity IDs exposed by the mock data layer
+    entityIDs = dataAccessLayerMetadata.entityIDs;
+
+    // save a reference to the `_id` supported by the mock data layer
+    databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID;
+
+    // create a resolver simulator, using the data access layer and an arbitrary component instance ID
+    simulator = new Simulator({
+      databaseDocumentID,
+      dataAccessLayer,
+      resolverComponentInstanceID,
+      indices: [],
+    });
+  });
+
+  it('should render no processes', async () => {
+    await expect(
+      simulator.map(() => ({
+        processes: simulator.processNodeElements().length,
+      }))
+    ).toYieldEqualTo({
+      processes: 0,
+    });
+  });
+
+  describe("when rerendered with the `indices` prop set to `['awesome_index'`]", () => {
+    beforeEach(async () => {
+      simulator.indices = ['awesome_index'];
+    });
+    // Combining assertions here for performance. Unfortunately, Enzyme + jsdom + React is slow.
+    it(`should have 3 nodes, with the entityID's 'origin', 'firstChild', and 'secondChild'. 'origin' should be selected when the simulator has the right indices`, async () => {
+      await expect(
+        simulator.map(() => ({
+          selectedOriginCount: simulator.selectedProcessNode(entityIDs.origin).length,
+          unselectedFirstChildCount: simulator.unselectedProcessNode(entityIDs.firstChild).length,
+          unselectedSecondChildCount: simulator.unselectedProcessNode(entityIDs.secondChild).length,
+          nodePrimaryButtonCount: simulator.testSubject('resolver:node:primary-button').length,
+        }))
+      ).toYieldEqualTo({
+        selectedOriginCount: 1,
+        unselectedFirstChildCount: 1,
+        unselectedSecondChildCount: 1,
+        nodePrimaryButtonCount: 3,
+      });
+    });
+  });
+});
+
 describe('Resolver, when analyzing a tree that has no ancestors and 2 children', () => {
   beforeEach(async () => {
     // create a mock data access layer
@@ -31,7 +88,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children',
     databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID;
 
     // create a resolver simulator, using the data access layer and an arbitrary component instance ID
-    simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID });
+    simulator = new Simulator({
+      databaseDocumentID,
+      dataAccessLayer,
+      resolverComponentInstanceID,
+      indices: [],
+    });
   });
 
   describe('when it has loaded', () => {
@@ -159,7 +221,12 @@ describe('Resolver, when analyzing a tree that has two related events for the or
     databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID;
 
     // create a resolver simulator, using the data access layer and an arbitrary component instance ID
-    simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID });
+    simulator = new Simulator({
+      databaseDocumentID,
+      dataAccessLayer,
+      resolverComponentInstanceID,
+      indices: [],
+    });
   });
 
   describe('when it has loaded', () => {
diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx
index 6497cc297198..95fe68d95d70 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx
@@ -34,6 +34,7 @@ describe('graph controls: when relsover is loaded with an origin node', () => {
       dataAccessLayer,
       databaseDocumentID,
       resolverComponentInstanceID,
+      indices: [],
     });
     originEntityID = entityIDs.origin;
 
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx
index 1add907ae933..7021e476e643 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx
@@ -49,6 +49,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an
           dataAccessLayer,
           resolverComponentInstanceID,
           history: memoryHistory,
+          indices: [],
         });
         return simulatorInstance;
       }
diff --git a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts
index a86237e0e2b4..e42de5009a0f 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts
@@ -29,7 +29,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children',
     databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID;
 
     // create a resolver simulator, using the data access layer and an arbitrary component instance ID
-    simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID });
+    simulator = new Simulator({
+      databaseDocumentID,
+      dataAccessLayer,
+      resolverComponentInstanceID,
+      indices: [],
+    });
   });
 
   describe("when the second child node's first button has been clicked", () => {
diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx
index c357ee18acfe..d8d8de640d78 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx
@@ -26,6 +26,7 @@ describe('Resolver: data loading and resolution states', () => {
         dataAccessLayer,
         databaseDocumentID,
         resolverComponentInstanceID,
+        indices: [],
       });
     });
 
@@ -56,6 +57,7 @@ describe('Resolver: data loading and resolution states', () => {
         dataAccessLayer,
         databaseDocumentID,
         resolverComponentInstanceID,
+        indices: [],
       });
     });
 
@@ -85,6 +87,7 @@ describe('Resolver: data loading and resolution states', () => {
         dataAccessLayer,
         databaseDocumentID,
         resolverComponentInstanceID,
+        indices: [],
       });
     });
 
@@ -114,6 +117,7 @@ describe('Resolver: data loading and resolution states', () => {
         dataAccessLayer,
         databaseDocumentID,
         resolverComponentInstanceID,
+        indices: [],
       });
     });
 
@@ -145,6 +149,7 @@ describe('Resolver: data loading and resolution states', () => {
         dataAccessLayer,
         databaseDocumentID,
         resolverComponentInstanceID,
+        indices: [],
       });
     });
 
diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx
index aa845e7283eb..f4d471b384b3 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx
@@ -31,7 +31,7 @@ export const ResolverWithoutProviders = React.memo(
    * Use `forwardRef` so that the `Simulator` used in testing can access the top level DOM element.
    */
   React.forwardRef(function (
-    { className, databaseDocumentID, resolverComponentInstanceID }: ResolverProps,
+    { className, databaseDocumentID, resolverComponentInstanceID, indices }: ResolverProps,
     refToForward
   ) {
     useResolverQueryParamCleaner();
@@ -39,7 +39,7 @@ export const ResolverWithoutProviders = React.memo(
      * This is responsible for dispatching actions that include any external data.
      * `databaseDocumentID`
      */
-    useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID });
+    useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID, indices });
 
     const { timestamp } = useContext(SideEffectContext);
 
@@ -69,8 +69,8 @@ export const ResolverWithoutProviders = React.memo(
       },
       [cameraRef, refToForward]
     );
-    const isLoading = useSelector(selectors.isLoading);
-    const hasError = useSelector(selectors.hasError);
+    const isLoading = useSelector(selectors.isTreeLoading);
+    const hasError = useSelector(selectors.hadErrorLoadingTree);
     const activeDescendantId = useSelector(selectors.ariaActiveDescendant);
     const { colorMap } = useResolverTheme();
 
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx
index 630ee2f7ff7f..495cd238d22f 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx
@@ -20,6 +20,7 @@ import { mock as mockResolverTree } from '../models/resolver_tree';
 import { ResolverAction } from '../store/actions';
 import { createStore } from 'redux';
 import { resolverReducer } from '../store/reducer';
+import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters';
 
 describe('useCamera on an unpainted element', () => {
   let element: HTMLElement;
@@ -181,7 +182,7 @@ describe('useCamera on an unpainted element', () => {
         if (tree !== null) {
           const serverResponseAction: ResolverAction = {
             type: 'serverReturnedResolverData',
-            payload: { result: tree, databaseDocumentID: '' },
+            payload: { result: tree, parameters: mockTreeFetcherParameters() },
           };
           act(() => {
             store.dispatch(serverResponseAction);
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts b/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts
index eaba4438bb1f..7f3cdcbec76a 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts
+++ b/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts
@@ -15,19 +15,21 @@ import { useResolverDispatch } from './use_resolver_dispatch';
 export function useStateSyncingActions({
   databaseDocumentID,
   resolverComponentInstanceID,
+  indices,
 }: {
   /**
    * The `_id` of an event in ES. Used to determine the origin of the Resolver graph.
    */
-  databaseDocumentID?: string;
+  databaseDocumentID: string;
   resolverComponentInstanceID: string;
+  indices: string[];
 }) {
   const dispatch = useResolverDispatch();
   const locationSearch = useLocation().search;
   useLayoutEffect(() => {
     dispatch({
       type: 'appReceivedNewExternalProperties',
-      payload: { databaseDocumentID, resolverComponentInstanceID, locationSearch },
+      payload: { databaseDocumentID, resolverComponentInstanceID, locationSearch, indices },
     });
-  }, [dispatch, databaseDocumentID, resolverComponentInstanceID, locationSearch]);
+  }, [dispatch, databaseDocumentID, resolverComponentInstanceID, locationSearch, indices]);
 }
diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts
index 097166a9c866..08e9fb854e5a 100644
--- a/x-pack/plugins/security_solution/public/shared_imports.ts
+++ b/x-pack/plugins/security_solution/public/shared_imports.ts
@@ -21,6 +21,7 @@ export {
   UseMultiFields,
   useForm,
   useFormContext,
+  useFormData,
   ValidationFunc,
   VALIDATION_TYPES,
 } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx
index ededf7015296..dc9557da70f9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx
@@ -19,7 +19,7 @@ import styled from 'styled-components';
 
 import { FULL_SCREEN } from '../timeline/body/column_headers/translations';
 import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations';
-import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants';
+import { DEFAULT_INDEX_KEY, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants';
 import { useFullScreen } from '../../../common/containers/use_full_screen';
 import { State } from '../../../common/store';
 import { TimelineId, TimelineType } from '../../../../common/types/timeline';
@@ -33,6 +33,8 @@ import { Resolver } from '../../../resolver/view';
 import { useAllCasesModal } from '../../../cases/components/use_all_cases_modal';
 
 import * as i18n from './translations';
+import { useUiSetting$ } from '../../../common/lib/kibana';
+import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
 
 const OverlayContainer = styled.div`
   height: 100%;
@@ -137,6 +139,16 @@ const GraphOverlayComponent = ({
     globalFullScreen,
   ]);
 
+  const { signalIndexName } = useSignalIndex();
+  const [siemDefaultIndices] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
+  const indices: string[] | null = useMemo(() => {
+    if (signalIndexName === null) {
+      return null;
+    } else {
+      return [...siemDefaultIndices, signalIndexName];
+    }
+  }, [signalIndexName, siemDefaultIndices]);
+
   return (
     <OverlayContainer>
       <EuiHorizontalRule margin="none" />
@@ -178,10 +190,13 @@ const GraphOverlayComponent = ({
       </EuiFlexGroup>
 
       <EuiHorizontalRule margin="none" />
-      <StyledResolver
-        databaseDocumentID={graphEventId}
-        resolverComponentInstanceID={currentTimeline.id}
-      />
+      {graphEventId !== undefined && indices !== null && (
+        <StyledResolver
+          databaseDocumentID={graphEventId}
+          resolverComponentInstanceID={currentTimeline.id}
+          indices={indices}
+        />
+      )}
       <AllCasesModal />
     </OverlayContainer>
   );
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx
index 2792b264ba7e..d01e8634a489 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx
@@ -102,12 +102,10 @@ const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowser
   const hideFieldBrowser = useCallback(() => setShow(false), []);
 
   const handleDisableAll = useCallback(() => {
-    // eslint-disable-next-line no-unused-expressions
     tableRef?.current?.setSelection([]);
   }, [tableRef]);
 
   const handleEnableAll = useCallback(() => {
-    // eslint-disable-next-line no-unused-expressions
     tableRef?.current?.setSelection(renderers);
   }, [tableRef]);
 
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx
index 7baa7c42fb45..f1414724e243 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx
@@ -88,7 +88,7 @@ const RowRenderersBrowserComponent = React.forwardRef(
       (item: RowRendererOption) => () => {
         const newSelection = xor([item], notExcludedRowRenderers);
         // @ts-expect-error
-        ref?.current?.setSelection(newSelection); // eslint-disable-line no-unused-expressions
+        ref?.current?.setSelection(newSelection);
       },
       [notExcludedRowRenderers, ref]
     );
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts
index bcac559d61f7..510bb6c54555 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts
@@ -18,11 +18,6 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
       query: { _id, indices },
     } = request;
 
-    const siemClient = context.securitySolution!.getAppClient();
-    const queryIndices = indices;
-    // if the alert was promoted by a rule it will exist in the signals index so search there too
-    queryIndices.push(siemClient.getSignalsIndex());
-
     /**
      * A safe type for the response based on the semantics of the query.
      * We specify _source, asking for `process.entity_id` and we only
@@ -36,8 +31,8 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
           | [
               {
                 _source: {
-                  process: {
-                    entity_id: string;
+                  process?: {
+                    entity_id?: string;
                   };
                 };
               }
@@ -49,7 +44,7 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
       'search',
       {
         ignoreUnavailable: true,
-        index: queryIndices,
+        index: indices,
         body: {
           // only return process.entity_id
           _source: 'process.entity_id',
@@ -64,19 +59,6 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
                     values: _id,
                   },
                 },
-                {
-                  exists: {
-                    // only return documents that have process.entity_id
-                    field: 'process.entity_id',
-                  },
-                },
-                {
-                  bool: {
-                    must_not: {
-                      term: { 'process.entity_id': '' },
-                    },
-                  },
-                },
               ],
             },
           },
@@ -85,15 +67,13 @@ export function handleEntities(): RequestHandler<unknown, TypeOf<typeof validate
     );
 
     const responseBody: ResolverEntityIndex = [];
-    for (const {
-      _source: {
-        // eslint-disable-next-line @typescript-eslint/naming-convention
-        process: { entity_id },
-      },
-    } of queryResponse.hits.hits) {
-      responseBody.push({
-        entity_id,
-      });
+    for (const hit of queryResponse.hits.hits) {
+      // check that the field is defined and that is not an empty string
+      if (hit._source.process?.entity_id) {
+        responseBody.push({
+          entity_id: hit._source.process.entity_id,
+        });
+      }
     }
     return response.ok({ body: responseBody });
   };
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts
index ac5132d93a06..be6e57aee6d0 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts
@@ -28,11 +28,12 @@ export const setSignalsStatusRoute = (router: IRouter) => {
       },
     },
     async (context, request, response) => {
-      const { signal_ids: signalIds, query, status } = request.body;
+      const { conflicts, signal_ids: signalIds, query, status } = request.body;
       const clusterClient = context.core.elasticsearch.legacy.client;
       const siemClient = context.securitySolution?.getAppClient();
       const siemResponse = buildSiemResponse(response);
       const validationErrors = setSignalStatusValidateTypeDependents(request.body);
+
       if (validationErrors.length) {
         return siemResponse.error({ statusCode: 400, body: validationErrors });
       }
@@ -55,6 +56,7 @@ export const setSignalsStatusRoute = (router: IRouter) => {
       try {
         const result = await clusterClient.callAsCurrentUser('updateByQuery', {
           index: siemClient.getSignalsIndex(),
+          conflicts: conflicts ?? 'abort',
           refresh: 'wait_for',
           body: {
             script: {
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts
index 5c29d2747f68..355082402847 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts
@@ -5,8 +5,8 @@
  */
 import { set } from '@elastic/safer-lodash-set/fp';
 import { get, has, head } from 'lodash/fp';
+import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields';
 import { HostsEdges } from '../../../../../../common/search_strategy/security_solution/hosts';
-import { hostFieldsMap } from '../../../../../lib/ecs_fields';
 
 import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types';
 
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts
index 35e4d2cc8e1f..df300c85e300 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts
@@ -6,14 +6,14 @@
 
 import { isEmpty } from 'lodash/fp';
 
-import { AuthenticationsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts/authentications';
+import { HostAuthenticationsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts/authentications';
 import { sourceFieldsMap, hostFieldsMap } from '../../../../../../../common/ecs/ecs_fields';
 
 import { createQueryFilterClauses } from '../../../../../../utils/build_query';
 import { reduceFields } from '../../../../../../utils/build_query/reduce_fields';
-import { extendMap } from '../../../../../../lib/ecs_fields/extend_map';
 
 import { authenticationFields } from '../helpers';
+import { extendMap } from '../../../../../../../common/ecs/ecs_fields/extend_map';
 
 export const auditdFieldsMap: Readonly<Record<string, string>> = {
   latest: '@timestamp',
@@ -31,7 +31,7 @@ export const buildQuery = ({
   pagination: { querySize },
   defaultIndex,
   docValueFields,
-}: AuthenticationsRequestOptions) => {
+}: HostAuthenticationsRequestOptions) => {
   const esFields = reduceFields(authenticationFields, { ...hostFieldsMap, ...sourceFieldsMap });
 
   const filter = [
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx
index 200818c40dec..ded9a7917d92 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx
@@ -12,8 +12,8 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants
 import {
   HostsQueries,
   AuthenticationsEdges,
-  AuthenticationsRequestOptions,
-  AuthenticationsStrategyResponse,
+  HostAuthenticationsRequestOptions,
+  HostAuthenticationsStrategyResponse,
   AuthenticationHit,
 } from '../../../../../../common/search_strategy/security_solution/hosts';
 
@@ -23,7 +23,7 @@ import { auditdFieldsMap, buildQuery as buildAuthenticationQuery } from './dsl/q
 import { formatAuthenticationData, getHits } from './helpers';
 
 export const authentications: SecuritySolutionFactory<HostsQueries.authentications> = {
-  buildDsl: (options: AuthenticationsRequestOptions) => {
+  buildDsl: (options: HostAuthenticationsRequestOptions) => {
     if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) {
       throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`);
     }
@@ -31,9 +31,9 @@ export const authentications: SecuritySolutionFactory<HostsQueries.authenticatio
     return buildAuthenticationQuery(options);
   },
   parse: async (
-    options: AuthenticationsRequestOptions,
+    options: HostAuthenticationsRequestOptions,
     response: IEsSearchResponse<unknown>
-  ): Promise<AuthenticationsStrategyResponse> => {
+  ): Promise<HostAuthenticationsStrategyResponse> => {
     const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination;
     const totalCount = getOr(0, 'aggregations.user_count.value', response.rawResponse);
 
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts
index 48e210d82291..56f7aec2327a 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts
@@ -5,11 +5,11 @@
  */
 import { set } from '@elastic/safer-lodash-set/fp';
 import { get, has, head } from 'lodash/fp';
+import { hostFieldsMap } from '../../../../../common/ecs/ecs_fields';
 import {
   HostsEdges,
   HostItem,
 } from '../../../../../common/search_strategy/security_solution/hosts';
-import { hostFieldsMap } from '../../../../lib/ecs_fields';
 
 import { HostAggEsItem, HostBuckets, HostValue } from '../../../../lib/hosts/types';
 
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts
index 6585abde6028..38d81c229ac5 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts
@@ -13,11 +13,13 @@ import { SecuritySolutionFactory } from '../types';
 import { allHosts } from './all';
 import { overviewHost } from './overview';
 import { firstLastSeenHost } from './last_first_seen';
+import { uncommonProcesses } from './uncommon_processes';
 import { authentications } from './authentications';
 
 export const hostsFactory: Record<HostsQueries, SecuritySolutionFactory<FactoryQueryTypes>> = {
   [HostsQueries.hosts]: allHosts,
   [HostsQueries.hostOverview]: overviewHost,
   [HostsQueries.firstLastSeen]: firstLastSeenHost,
+  [HostsQueries.uncommonProcesses]: uncommonProcesses,
   [HostsQueries.authentications]: authentications,
 };
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts
index c7b0d8acc878..ed705e7f6ad5 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts
@@ -5,8 +5,8 @@
  */
 import { set } from '@elastic/safer-lodash-set/fp';
 import { get, has, head } from 'lodash/fp';
+import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields';
 import { HostItem } from '../../../../../../common/search_strategy/security_solution/hosts';
-import { hostFieldsMap } from '../../../../../lib/ecs_fields';
 
 import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types';
 
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts
index 913bc90df04b..85cc87414c38 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts
@@ -5,8 +5,8 @@
  */
 
 import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common';
+import { cloudFieldsMap, hostFieldsMap } from '../../../../../../common/ecs/ecs_fields';
 import { HostOverviewRequestOptions } from '../../../../../../common/search_strategy/security_solution';
-import { cloudFieldsMap, hostFieldsMap } from '../../../../../lib/ecs_fields';
 import { buildFieldsTermAggregation } from '../../../../../lib/hosts/helpers';
 import { reduceFields } from '../../../../../utils/build_query/reduce_fields';
 import { HOST_FIELDS } from './helpers';
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts
new file mode 100644
index 000000000000..2e2d889dda11
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts
@@ -0,0 +1,226 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { createQueryFilterClauses } from '../../../../../../utils/build_query';
+import { reduceFields } from '../../../../../../utils/build_query/reduce_fields';
+import {
+  hostFieldsMap,
+  processFieldsMap,
+  userFieldsMap,
+} from '../../../../../../../common/ecs/ecs_fields';
+import { RequestOptionsPaginated } from '../../../../../../../common/search_strategy/security_solution';
+import { uncommonProcessesFields } from '../helpers';
+
+export const buildQuery = ({
+  defaultIndex,
+  filterQuery,
+  pagination: { querySize },
+  timerange: { from, to },
+}: RequestOptionsPaginated) => {
+  const processUserFields = reduceFields(uncommonProcessesFields, {
+    ...processFieldsMap,
+    ...userFieldsMap,
+  });
+  const hostFields = reduceFields(uncommonProcessesFields, hostFieldsMap);
+  const filter = [
+    ...createQueryFilterClauses(filterQuery),
+    {
+      range: {
+        '@timestamp': {
+          gte: from,
+          lte: to,
+          format: 'strict_date_optional_time',
+        },
+      },
+    },
+  ];
+
+  const agg = {
+    process_count: {
+      cardinality: {
+        field: 'process.name',
+      },
+    },
+  };
+
+  const dslQuery = {
+    allowNoIndices: true,
+    index: defaultIndex,
+    ignoreUnavailable: true,
+    body: {
+      aggregations: {
+        ...agg,
+        group_by_process: {
+          terms: {
+            size: querySize,
+            field: 'process.name',
+            order: [
+              {
+                host_count: 'asc',
+              },
+              {
+                _count: 'asc',
+              },
+              {
+                _key: 'asc',
+              },
+            ],
+          },
+          aggregations: {
+            process: {
+              top_hits: {
+                size: 1,
+                sort: [{ '@timestamp': { order: 'desc' } }],
+                _source: processUserFields,
+              },
+            },
+            host_count: {
+              cardinality: {
+                field: 'host.name',
+              },
+            },
+            hosts: {
+              terms: {
+                field: 'host.name',
+              },
+              aggregations: {
+                host: {
+                  top_hits: {
+                    size: 1,
+                    _source: hostFields,
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+      query: {
+        bool: {
+          should: [
+            {
+              bool: {
+                filter: [
+                  {
+                    term: {
+                      'agent.type': 'auditbeat',
+                    },
+                  },
+                  {
+                    term: {
+                      'event.module': 'auditd',
+                    },
+                  },
+                  {
+                    term: {
+                      'event.action': 'executed',
+                    },
+                  },
+                ],
+              },
+            },
+            {
+              bool: {
+                filter: [
+                  {
+                    term: {
+                      'agent.type': 'auditbeat',
+                    },
+                  },
+                  {
+                    term: {
+                      'event.module': 'system',
+                    },
+                  },
+                  {
+                    term: {
+                      'event.dataset': 'process',
+                    },
+                  },
+                  {
+                    term: {
+                      'event.action': 'process_started',
+                    },
+                  },
+                ],
+              },
+            },
+            {
+              bool: {
+                filter: [
+                  {
+                    term: {
+                      'agent.type': 'winlogbeat',
+                    },
+                  },
+                  {
+                    term: {
+                      'event.code': '4688',
+                    },
+                  },
+                ],
+              },
+            },
+            {
+              bool: {
+                filter: [
+                  {
+                    term: {
+                      'winlog.event_id': 1,
+                    },
+                  },
+                  {
+                    term: {
+                      'winlog.channel': 'Microsoft-Windows-Sysmon/Operational',
+                    },
+                  },
+                ],
+              },
+            },
+            {
+              bool: {
+                filter: [
+                  {
+                    term: {
+                      'event.type': 'process_start',
+                    },
+                  },
+                  {
+                    term: {
+                      'event.category': 'process',
+                    },
+                  },
+                ],
+              },
+            },
+            {
+              bool: {
+                filter: [
+                  {
+                    term: {
+                      'event.category': 'process',
+                    },
+                  },
+                  {
+                    term: {
+                      'event.type': 'start',
+                    },
+                  },
+                ],
+              },
+            },
+          ],
+          minimum_should_match: 1,
+          filter,
+        },
+      },
+    },
+    size: 0,
+    track_total_hits: false,
+  };
+
+  return dslQuery;
+};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts
new file mode 100644
index 000000000000..5c3d76175b7e
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts
@@ -0,0 +1,94 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { get, getOr } from 'lodash/fp';
+import { set } from '@elastic/safer-lodash-set/fp';
+
+import { mergeFieldsWithHit } from '../../../../../utils/build_query';
+import {
+  ProcessHits,
+  UncommonProcessesEdges,
+  UncommonProcessHit,
+} from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes';
+import { toArray } from '../../../../helpers/to_array';
+import { HostHits } from '../../../../../../common/search_strategy';
+
+export const uncommonProcessesFields = [
+  '_id',
+  'instances',
+  'process.args',
+  'process.name',
+  'user.id',
+  'user.name',
+  'hosts.name',
+];
+
+export const getHits = (buckets: readonly UncommonProcessBucket[]): readonly UncommonProcessHit[] =>
+  buckets.map((bucket: Readonly<UncommonProcessBucket>) => ({
+    _id: bucket.process.hits.hits[0]._id,
+    _index: bucket.process.hits.hits[0]._index,
+    _type: bucket.process.hits.hits[0]._type,
+    _score: bucket.process.hits.hits[0]._score,
+    _source: bucket.process.hits.hits[0]._source,
+    sort: bucket.process.hits.hits[0].sort,
+    cursor: bucket.process.hits.hits[0].cursor,
+    total: bucket.process.hits.total,
+    host: getHosts(bucket.hosts.buckets),
+  }));
+
+export interface UncommonProcessBucket {
+  key: string;
+  hosts: {
+    buckets: Array<{ key: string; host: HostHits }>;
+  };
+  process: ProcessHits;
+}
+
+export const getHosts = (buckets: ReadonlyArray<{ key: string; host: HostHits }>) =>
+  buckets.map((bucket) => {
+    const source = get('host.hits.hits[0]._source', bucket);
+    return {
+      id: [bucket.key],
+      name: get('host.name', source),
+    };
+  });
+
+export const formatUncommonProcessesData = (
+  fields: readonly string[],
+  hit: UncommonProcessHit,
+  fieldMap: Readonly<Record<string, string>>
+): UncommonProcessesEdges =>
+  fields.reduce<UncommonProcessesEdges>(
+    (flattenedFields, fieldName) => {
+      flattenedFields.node._id = hit._id;
+      flattenedFields.node.instances = getOr(0, 'total.value', hit);
+      flattenedFields.node.hosts = hit.host;
+
+      if (hit.cursor) {
+        flattenedFields.cursor.value = hit.cursor;
+      }
+
+      const mergedResult = mergeFieldsWithHit(fieldName, flattenedFields, fieldMap, hit);
+      let fieldPath = `node.${fieldName}`;
+      let fieldValue = get(fieldPath, mergedResult);
+      if (fieldPath === 'node.hosts.name') {
+        fieldPath = `node.hosts.0.name`;
+        fieldValue = get(fieldPath, mergedResult);
+      }
+      return set(fieldPath, toArray(fieldValue), mergedResult);
+    },
+    {
+      node: {
+        _id: '',
+        instances: 0,
+        process: {},
+        hosts: [],
+      },
+      cursor: {
+        value: '',
+        tiebreaker: null,
+      },
+    }
+  );
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts
new file mode 100644
index 000000000000..fcc76eebe4cf
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getOr } from 'lodash/fp';
+
+import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common';
+
+import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants';
+import { HostsQueries } from '../../../../../../common/search_strategy/security_solution';
+import { processFieldsMap, userFieldsMap } from '../../../../../../common/ecs/ecs_fields';
+import {
+  HostUncommonProcessesRequestOptions,
+  HostUncommonProcessesStrategyResponse,
+} from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes';
+
+import { inspectStringifyObject } from '../../../../../utils/build_query';
+
+import { SecuritySolutionFactory } from '../../types';
+import { buildQuery } from './dsl/query.dsl';
+import { formatUncommonProcessesData, getHits, uncommonProcessesFields } from './helpers';
+
+export const uncommonProcesses: SecuritySolutionFactory<HostsQueries.uncommonProcesses> = {
+  buildDsl: (options: HostUncommonProcessesRequestOptions) => {
+    if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) {
+      throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`);
+    }
+    return buildQuery(options);
+  },
+  parse: async (
+    options: HostUncommonProcessesRequestOptions,
+    response: IEsSearchResponse<unknown>
+  ): Promise<HostUncommonProcessesStrategyResponse> => {
+    const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination;
+    const totalCount = getOr(0, 'aggregations.process_count.value', response.rawResponse);
+    const buckets = getOr([], 'aggregations.group_by_process.buckets', response.rawResponse);
+    const hits = getHits(buckets);
+
+    const uncommonProcessesEdges = hits.map((hit) =>
+      formatUncommonProcessesData(uncommonProcessesFields, hit, {
+        ...processFieldsMap,
+        ...userFieldsMap,
+      })
+    );
+
+    const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount;
+    const edges = uncommonProcessesEdges.splice(cursorStart, querySize - cursorStart);
+    const inspect = {
+      dsl: [inspectStringifyObject(buildQuery(options))],
+      response: [inspectStringifyObject(response)],
+    };
+
+    const showMorePagesIndicator = totalCount > fakeTotalCount;
+    return {
+      ...response,
+      edges,
+      inspect,
+      pageInfo: {
+        activePage: activePage ? activePage : 0,
+        fakeTotalCount,
+        showMorePagesIndicator,
+      },
+      totalCount,
+    };
+  },
+};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts
index 9e73312bdb8e..c5c98e5facbd 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts
@@ -13,9 +13,11 @@ import { SecuritySolutionFactory } from '../types';
 import { networkHttp } from './http';
 import { networkTls } from './tls';
 import { networkTopCountries } from './top_countries';
+import { networkTopNFlow } from './top_n_flow';
 
 export const networkFactory: Record<NetworkQueries, SecuritySolutionFactory<FactoryQueryTypes>> = {
   [NetworkQueries.http]: networkHttp,
   [NetworkQueries.tls]: networkTls,
   [NetworkQueries.topCountries]: networkTopCountries,
+  [NetworkQueries.topNFlow]: networkTopNFlow,
 };
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts
new file mode 100644
index 000000000000..720661e12bd9
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts
@@ -0,0 +1,126 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getOr } from 'lodash/fp';
+
+import { assertUnreachable } from '../../../../../../common/utility_types';
+import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common';
+import {
+  Direction,
+  GeoItem,
+  SortField,
+  NetworkTopNFlowBuckets,
+  NetworkTopNFlowEdges,
+  NetworkTopNFlowRequestOptions,
+  NetworkTopTablesFields,
+  FlowTargetSourceDest,
+  AutonomousSystemItem,
+} from '../../../../../../common/search_strategy';
+import { getOppositeField } from '../helpers';
+
+export const getTopNFlowEdges = (
+  response: IEsSearchResponse<unknown>,
+  options: NetworkTopNFlowRequestOptions
+): NetworkTopNFlowEdges[] =>
+  formatTopNFlowEdges(
+    getOr([], `aggregations.${options.flowTarget}.buckets`, response.rawResponse),
+    options.flowTarget
+  );
+
+const formatTopNFlowEdges = (
+  buckets: NetworkTopNFlowBuckets[],
+  flowTarget: FlowTargetSourceDest
+): NetworkTopNFlowEdges[] =>
+  buckets.map((bucket: NetworkTopNFlowBuckets) => ({
+    node: {
+      _id: bucket.key,
+      [flowTarget]: {
+        domain: bucket.domain.buckets.map((bucketDomain) => bucketDomain.key),
+        ip: bucket.key,
+        location: getGeoItem(bucket),
+        autonomous_system: getAsItem(bucket),
+        flows: getOr(0, 'flows.value', bucket),
+        [`${getOppositeField(flowTarget)}_ips`]: getOr(
+          0,
+          `${getOppositeField(flowTarget)}_ips.value`,
+          bucket
+        ),
+      },
+      network: {
+        bytes_in: getOr(0, 'bytes_in.value', bucket),
+        bytes_out: getOr(0, 'bytes_out.value', bucket),
+      },
+    },
+    cursor: {
+      value: bucket.key,
+      tiebreaker: null,
+    },
+  }));
+
+const getFlowTargetFromString = (flowAsString: string) =>
+  flowAsString === 'source' ? FlowTargetSourceDest.source : FlowTargetSourceDest.destination;
+
+const getGeoItem = (result: NetworkTopNFlowBuckets): GeoItem | null =>
+  result.location.top_geo.hits.hits.length > 0 && result.location.top_geo.hits.hits[0]._source
+    ? {
+        geo: getOr(
+          '',
+          `location.top_geo.hits.hits[0]._source.${
+            Object.keys(result.location.top_geo.hits.hits[0]._source)[0]
+          }.geo`,
+          result
+        ),
+        flowTarget: getFlowTargetFromString(
+          Object.keys(result.location.top_geo.hits.hits[0]._source)[0]
+        ),
+      }
+    : null;
+
+const getAsItem = (result: NetworkTopNFlowBuckets): AutonomousSystemItem | null =>
+  result.autonomous_system.top_as.hits.hits.length > 0 &&
+  result.autonomous_system.top_as.hits.hits[0]._source
+    ? {
+        number: getOr(
+          null,
+          `autonomous_system.top_as.hits.hits[0]._source.${
+            Object.keys(result.autonomous_system.top_as.hits.hits[0]._source)[0]
+          }.as.number`,
+          result
+        ),
+        name: getOr(
+          '',
+          `autonomous_system.top_as.hits.hits[0]._source.${
+            Object.keys(result.autonomous_system.top_as.hits.hits[0]._source)[0]
+          }.as.organization.name`,
+          result
+        ),
+      }
+    : null;
+
+type QueryOrder =
+  | { bytes_in: Direction }
+  | { bytes_out: Direction }
+  | { flows: Direction }
+  | { destination_ips: Direction }
+  | { source_ips: Direction };
+
+export const getQueryOrder = (
+  networkTopNFlowSortField: SortField<NetworkTopTablesFields>
+): QueryOrder => {
+  switch (networkTopNFlowSortField.field) {
+    case NetworkTopTablesFields.bytes_in:
+      return { bytes_in: networkTopNFlowSortField.direction };
+    case NetworkTopTablesFields.bytes_out:
+      return { bytes_out: networkTopNFlowSortField.direction };
+    case NetworkTopTablesFields.flows:
+      return { flows: networkTopNFlowSortField.direction };
+    case NetworkTopTablesFields.destination_ips:
+      return { destination_ips: networkTopNFlowSortField.direction };
+    case NetworkTopTablesFields.source_ips:
+      return { source_ips: networkTopNFlowSortField.direction };
+  }
+  assertUnreachable(networkTopNFlowSortField.field);
+};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/index.ts
new file mode 100644
index 000000000000..198368d98180
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/index.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getOr } from 'lodash/fp';
+
+import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common';
+
+import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants';
+import {
+  NetworkTopNFlowStrategyResponse,
+  NetworkQueries,
+  NetworkTopNFlowRequestOptions,
+  NetworkTopNFlowEdges,
+} from '../../../../../../common/search_strategy/security_solution/network';
+
+import { inspectStringifyObject } from '../../../../../utils/build_query';
+import { SecuritySolutionFactory } from '../../types';
+
+import { getTopNFlowEdges } from './helpers';
+import { buildTopNFlowQuery } from './query.top_n_flow_network.dsl';
+
+export const networkTopNFlow: SecuritySolutionFactory<NetworkQueries.topNFlow> = {
+  buildDsl: (options: NetworkTopNFlowRequestOptions) => {
+    if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) {
+      throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`);
+    }
+    return buildTopNFlowQuery(options);
+  },
+  parse: async (
+    options: NetworkTopNFlowRequestOptions,
+    response: IEsSearchResponse<unknown>
+  ): Promise<NetworkTopNFlowStrategyResponse> => {
+    const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination;
+    const totalCount = getOr(0, 'aggregations.top_n_flow_count.value', response.rawResponse);
+    const networkTopNFlowEdges: NetworkTopNFlowEdges[] = getTopNFlowEdges(response, options);
+    const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount;
+    const edges = networkTopNFlowEdges.splice(cursorStart, querySize - cursorStart);
+    const inspect = {
+      dsl: [inspectStringifyObject(buildTopNFlowQuery(options))],
+    };
+    const showMorePagesIndicator = totalCount > fakeTotalCount;
+
+    return {
+      ...response,
+      edges,
+      inspect,
+      pageInfo: {
+        activePage: activePage ? activePage : 0,
+        fakeTotalCount,
+        showMorePagesIndicator,
+      },
+      totalCount,
+    };
+  },
+};
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/query.top_n_flow_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/query.top_n_flow_network.dsl.ts
new file mode 100644
index 000000000000..374dfa4d485f
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/query.top_n_flow_network.dsl.ts
@@ -0,0 +1,157 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+  SortField,
+  FlowTargetSourceDest,
+  NetworkTopTablesFields,
+  NetworkTopNFlowRequestOptions,
+} from '../../../../../../common/search_strategy';
+import { createQueryFilterClauses } from '../../../../../utils/build_query';
+import { getOppositeField } from '../helpers';
+import { getQueryOrder } from './helpers';
+
+const getCountAgg = (flowTarget: FlowTargetSourceDest) => ({
+  top_n_flow_count: {
+    cardinality: {
+      field: `${flowTarget}.ip`,
+    },
+  },
+});
+
+export const buildTopNFlowQuery = ({
+  defaultIndex,
+  filterQuery,
+  flowTarget,
+  sort,
+  pagination: { querySize },
+  timerange: { from, to },
+  ip,
+}: NetworkTopNFlowRequestOptions) => {
+  const filter = [
+    ...createQueryFilterClauses(filterQuery),
+    {
+      range: {
+        '@timestamp': { gte: from, lte: to, format: 'strict_date_optional_time' },
+      },
+    },
+  ];
+
+  const dslQuery = {
+    allowNoIndices: true,
+    index: defaultIndex,
+    ignoreUnavailable: true,
+    body: {
+      aggregations: {
+        ...getCountAgg(flowTarget),
+        ...getFlowTargetAggs(sort, flowTarget, querySize),
+      },
+      query: {
+        bool: ip
+          ? {
+              filter,
+              should: [
+                {
+                  term: {
+                    [`${getOppositeField(flowTarget)}.ip`]: ip,
+                  },
+                },
+              ],
+              minimum_should_match: 1,
+            }
+          : {
+              filter,
+            },
+      },
+    },
+    size: 0,
+    track_total_hits: false,
+  };
+  return dslQuery;
+};
+
+const getFlowTargetAggs = (
+  sort: SortField<NetworkTopTablesFields>,
+  flowTarget: FlowTargetSourceDest,
+  querySize: number
+) => ({
+  [flowTarget]: {
+    terms: {
+      field: `${flowTarget}.ip`,
+      size: querySize,
+      order: {
+        ...getQueryOrder(sort),
+      },
+    },
+    aggs: {
+      bytes_in: {
+        sum: {
+          field: `${getOppositeField(flowTarget)}.bytes`,
+        },
+      },
+      bytes_out: {
+        sum: {
+          field: `${flowTarget}.bytes`,
+        },
+      },
+      domain: {
+        terms: {
+          field: `${flowTarget}.domain`,
+          order: {
+            timestamp: 'desc',
+          },
+        },
+        aggs: {
+          timestamp: {
+            max: {
+              field: '@timestamp',
+            },
+          },
+        },
+      },
+      location: {
+        filter: {
+          exists: {
+            field: `${flowTarget}.geo`,
+          },
+        },
+        aggs: {
+          top_geo: {
+            top_hits: {
+              _source: `${flowTarget}.geo.*`,
+              size: 1,
+            },
+          },
+        },
+      },
+      autonomous_system: {
+        filter: {
+          exists: {
+            field: `${flowTarget}.as`,
+          },
+        },
+        aggs: {
+          top_as: {
+            top_hits: {
+              _source: `${flowTarget}.as.*`,
+              size: 1,
+            },
+          },
+        },
+      },
+      flows: {
+        cardinality: {
+          field: 'network.community_id',
+        },
+      },
+      [`${getOppositeField(flowTarget)}_ips`]: {
+        cardinality: {
+          field: `${getOppositeField(flowTarget)}.ip`,
+        },
+      },
+    },
+  },
+});
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index a745c2fc98b4..eacb1febd20f 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9468,8 +9468,6 @@
     "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.cancelButtonLabel": "キャンセル",
     "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.confirmationButtonLabel": "プロセッサーの削除",
     "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.titleText": "{type}プロセッサーの削除",
-    "xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel": "フィールド",
-    "xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError": "フィールド値が必要です。",
     "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel": "無効化",
     "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "値",
     "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "設定する値が必要です。",
@@ -9557,7 +9555,6 @@
     "xpack.lens.configure.editConfig": "構成の編集",
     "xpack.lens.configure.emptyConfig": "ここにフィールドをドロップ",
     "xpack.lens.dataPanelWrapper.switchDatasource": "データソースに切り替える",
-    "xpack.lens.datatable.columns": "フィールド",
     "xpack.lens.datatable.conjunctionSign": " と ",
     "xpack.lens.datatable.expressionHelpLabel": "データベースレンダー",
     "xpack.lens.datatable.label": "データテーブル",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 7b630e1c1348..bd30703dd5bd 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9474,8 +9474,6 @@
     "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.cancelButtonLabel": "取消",
     "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.confirmationButtonLabel": "删除处理器",
     "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.titleText": "删除 {type} 处理器",
-    "xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel": "字段",
-    "xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError": "字段值必填。",
     "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel": "覆盖",
     "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "值",
     "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "需要设置值。",
@@ -9563,7 +9561,6 @@
     "xpack.lens.configure.editConfig": "编辑配置",
     "xpack.lens.configure.emptyConfig": "将字段拖放到此处",
     "xpack.lens.dataPanelWrapper.switchDatasource": "切换到数据源",
-    "xpack.lens.datatable.columns": "字段",
     "xpack.lens.datatable.conjunctionSign": " &amp; ",
     "xpack.lens.datatable.expressionHelpLabel": "数据表呈现器",
     "xpack.lens.datatable.label": "数据表",
diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx
index a49251811239..16d0250c5721 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx
@@ -21,7 +21,12 @@ import {
   EuiLink,
 } from '@elastic/eui';
 import { FormattedMessage } from '@kbn/i18n/react';
-import { txtChangeButton, txtTriggerPickerHelpText, txtTriggerPickerLabel } from './i18n';
+import {
+  txtChangeButton,
+  txtTriggerPickerHelpText,
+  txtTriggerPickerLabel,
+  txtTriggerPickerHelpTooltip,
+} from './i18n';
 import './action_wizard.scss';
 import { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions';
 import { Trigger, TriggerId } from '../../../../../../src/plugins/ui_actions/public';
@@ -157,14 +162,17 @@ const TriggerPicker: React.FC<TriggerPickerProps> = ({
   const selectedTrigger = selectedTriggers ? selectedTriggers[0] : undefined;
   return (
     <EuiFormFieldset
+      data-test-subj={`triggerPicker`}
       legend={{
         children: (
           <EuiText size="s">
             <h5>
               <span>{txtTriggerPickerLabel}</span>{' '}
-              <EuiLink href={triggerPickerDocsLink} target={'blank'} external>
-                {txtTriggerPickerHelpText}
-              </EuiLink>
+              <EuiToolTip content={txtTriggerPickerHelpTooltip}>
+                <EuiLink href={triggerPickerDocsLink} target={'blank'} external>
+                  {txtTriggerPickerHelpText}
+                </EuiLink>
+              </EuiToolTip>
             </h5>
           </EuiText>
         ),
@@ -271,7 +279,7 @@ const SelectedActionFactory: React.FC<SelectedActionFactoryProps> = ({
           />
         </>
       )}
-      <EuiSpacer size="l" />
+      <EuiSpacer size="m" />
       <div>
         <actionFactory.ReactCollectConfig
           config={config}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts
index 678457f9794f..f494ecfb51f3 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts
@@ -16,13 +16,20 @@ export const txtChangeButton = i18n.translate(
 export const txtTriggerPickerLabel = i18n.translate(
   'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel',
   {
-    defaultMessage: 'Pick a trigger:',
+    defaultMessage: 'Show option on:',
   }
 );
 
 export const txtTriggerPickerHelpText = i18n.translate(
-  'xpack.uiActionsEnhanced.components.actionWizard.helpText',
+  'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpText',
   {
     defaultMessage: "What's this?",
   }
 );
+
+export const txtTriggerPickerHelpTooltip = i18n.translate(
+  'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip',
+  {
+    defaultMessage: 'Determines when the drilldown appears in context menu',
+  }
+);
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
index b708bbc57375..8154ec45b8ae 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
@@ -30,7 +30,7 @@ import {
   SerializedAction,
   SerializedEvent,
 } from '../../../dynamic_actions';
-import { ExtraActionFactoryContext } from '../types';
+import { ActionFactoryPlaceContext } from '../types';
 
 interface ConnectedFlyoutManageDrilldownsProps<
   ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
@@ -47,7 +47,7 @@ interface ConnectedFlyoutManageDrilldownsProps<
   /**
    * Extra action factory context passed into action factories CollectConfig, getIconType, getDisplayName and etc...
    */
-  extraContext?: ExtraActionFactoryContext<ActionFactoryContext>;
+  placeContext?: ActionFactoryPlaceContext<ActionFactoryContext>;
 }
 
 /**
@@ -81,8 +81,8 @@ export function createFlyoutManageDrilldowns({
     const isCreateOnly = props.viewMode === 'create';
 
     const factoryContext: BaseActionFactoryContext = useMemo(
-      () => ({ ...props.extraContext, triggers: props.supportedTriggers }),
-      [props.extraContext, props.supportedTriggers]
+      () => ({ ...props.placeContext, triggers: props.supportedTriggers }),
+      [props.placeContext, props.supportedTriggers]
     );
     const actionFactories = useCompatibleActionFactoriesForCurrentContext(
       allActionFactories,
@@ -137,7 +137,7 @@ export function createFlyoutManageDrilldowns({
     function mapToDrilldownToDrilldownListItem(drilldown: SerializedEvent): DrilldownListItem {
       const actionFactory = allActionFactoriesById[drilldown.action.factoryId];
       const drilldownFactoryContext: BaseActionFactoryContext = {
-        ...props.extraContext,
+        ...props.placeContext,
         triggers: drilldown.triggers as TriggerId[],
       };
       return {
@@ -204,7 +204,7 @@ export function createFlyoutManageDrilldowns({
               setRoute(Routes.Manage);
               setCurrentEditId(null);
             }}
-            extraActionFactoryContext={props.extraContext}
+            actionFactoryPlaceContext={props.placeContext}
             initialDrilldownWizardConfig={resolveInitialDrilldownWizardConfig()}
             supportedTriggers={props.supportedTriggers}
             getTrigger={getTrigger}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx
index a908d53bf6ae..c8e3f454bd53 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx
@@ -18,7 +18,7 @@ import {
 import { DrilldownHelloBar } from '../drilldown_hello_bar';
 import { ActionFactory, BaseActionFactoryContext } from '../../../dynamic_actions';
 import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
-import { ExtraActionFactoryContext } from '../types';
+import { ActionFactoryPlaceContext } from '../types';
 
 export interface DrilldownWizardConfig<ActionConfig extends object = object> {
   name: string;
@@ -44,7 +44,7 @@ export interface FlyoutDrilldownWizardProps<
   showWelcomeMessage?: boolean;
   onWelcomeHideClick?: () => void;
 
-  extraActionFactoryContext?: ExtraActionFactoryContext<ActionFactoryContext>;
+  actionFactoryPlaceContext?: ActionFactoryPlaceContext<ActionFactoryContext>;
 
   docsLink?: string;
 
@@ -143,7 +143,7 @@ export function FlyoutDrilldownWizard<CurrentActionConfig extends object = objec
   showWelcomeMessage = true,
   onWelcomeHideClick,
   drilldownActionFactories,
-  extraActionFactoryContext,
+  actionFactoryPlaceContext,
   docsLink,
   getTrigger,
   supportedTriggers,
@@ -152,16 +152,16 @@ export function FlyoutDrilldownWizard<CurrentActionConfig extends object = objec
     wizardConfig,
     { setActionFactory, setActionConfig, setName, setSelectedTriggers },
   ] = useWizardConfigState(
-    { ...extraActionFactoryContext, triggers: supportedTriggers },
+    { ...actionFactoryPlaceContext, triggers: supportedTriggers },
     initialDrilldownWizardConfig
   );
 
   const actionFactoryContext: BaseActionFactoryContext = useMemo(
     () => ({
-      ...extraActionFactoryContext,
+      ...actionFactoryPlaceContext,
       triggers: wizardConfig.selectedTriggers ?? [],
     }),
-    [extraActionFactoryContext, wizardConfig.selectedTriggers]
+    [actionFactoryPlaceContext, wizardConfig.selectedTriggers]
   );
 
   const isActionValid = (
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts
index 870b55c24fb5..811680bf380f 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts
@@ -10,6 +10,6 @@ import { BaseActionFactoryContext } from '../../dynamic_actions';
  * Interface used as piece of ActionFactoryContext that is passed in from drilldown wizard component to action factories
  * Omitted values are added inside the wizard and then full {@link BaseActionFactoryContext} passed into action factory methods
  */
-export type ExtraActionFactoryContext<
+export type ActionFactoryPlaceContext<
   ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
 > = Omit<ActionFactoryContext, 'triggers'>;
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md
new file mode 100644
index 000000000000..acad968fa46c
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md
@@ -0,0 +1 @@
+This directory contains reusable building blocks for creating custom URL drilldowns
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts
new file mode 100644
index 000000000000..70399617136b
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { UrlDrilldownCollectConfig } from './url_drilldown_collect_config/url_drilldown_collect_config';
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts
new file mode 100644
index 000000000000..78f7218dce22
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const txtUrlTemplatePlaceholder = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText',
+  {
+    defaultMessage: 'Example: {exampleUrl}',
+    values: {
+      exampleUrl: 'https://www.my-url.com/?{{event.key}}={{event.value}}',
+    },
+  }
+);
+
+export const txtUrlPreviewHelpText = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText',
+  {
+    defaultMessage: 'Please note that \\{\\{event.*\\}\\} variables replaced by dummy values.',
+  }
+);
+
+export const txtAddVariableButtonTitle = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle',
+  {
+    defaultMessage: 'Add variable',
+  }
+);
+
+export const txtUrlTemplateLabel = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel',
+  {
+    defaultMessage: 'Enter URL template:',
+  }
+);
+
+export const txtUrlTemplateSyntaxHelpLinkText = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText',
+  {
+    defaultMessage: 'Syntax help',
+  }
+);
+
+export const txtUrlTemplateVariablesHelpLinkText = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText',
+  {
+    defaultMessage: 'Help',
+  }
+);
+
+export const txtUrlTemplateVariablesFilterPlaceholderText = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText',
+  {
+    defaultMessage: 'Filter variables',
+  }
+);
+
+export const txtUrlTemplatePreviewLabel = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel',
+  {
+    defaultMessage: 'URL preview:',
+  }
+);
+
+export const txtUrlTemplatePreviewLinkText = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText',
+  {
+    defaultMessage: 'Preview',
+  }
+);
+
+export const txtUrlTemplateOpenInNewTab = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel',
+  {
+    defaultMessage: 'Open in new tab',
+  }
+);
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss
new file mode 100644
index 000000000000..475c3f2a915e
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss
@@ -0,0 +1,5 @@
+.uaeUrlDrilldownCollectConfig__urlTemplateFormRow {
+  .euiFormRow__label {
+    align-self: flex-end;
+  }
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx
new file mode 100644
index 000000000000..e6c9797623e9
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { UrlDrilldownConfig, UrlDrilldownScope } from '../../../types';
+import { UrlDrilldownCollectConfig } from '../url_drilldown_collect_config';
+
+export const Demo = () => {
+  const [config, onConfig] = React.useState<UrlDrilldownConfig>({
+    openInNewTab: false,
+    url: { template: '' },
+  });
+
+  const fakeScope: UrlDrilldownScope = {
+    kibanaUrl: 'http://localhost:5601/',
+    context: {
+      filters: [
+        {
+          query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
+          meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
+        },
+        {
+          query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
+          meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
+        },
+        {
+          query: { match: { _type: { query: 'nginx', type: 'phrase' } } },
+          meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
+        },
+      ],
+    },
+    event: {
+      key: 'fakeKey',
+      value: 'fakeValue',
+    },
+  };
+
+  return (
+    <>
+      <UrlDrilldownCollectConfig config={config} onConfig={onConfig} scope={fakeScope} />
+      {JSON.stringify(config)}
+    </>
+  );
+};
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx
new file mode 100644
index 000000000000..244ea9bd2a97
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { Demo } from './test_samples/demo';
+
+storiesOf('UrlDrilldownCollectConfig', module).add('default', () => <Demo />);
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx
new file mode 100644
index 000000000000..f55818379ef3
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Demo } from './test_samples/demo';
+import { cleanup, fireEvent, render } from '@testing-library/react/pure';
+import React from 'react';
+
+afterEach(cleanup);
+
+test('configure valid URL template', () => {
+  const screen = render(<Demo />);
+
+  const urlTemplate = 'https://elastic.co/?{{event.key}}={{event.value}}';
+  fireEvent.change(screen.getByLabelText(/Enter URL template/i), {
+    target: { value: urlTemplate },
+  });
+
+  const preview = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement;
+  expect(preview.value).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`);
+  expect(preview.disabled).toEqual(true);
+  const previewLink = screen.getByText('Preview') as HTMLAnchorElement;
+  expect(previewLink.href).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`);
+  expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`);
+});
+
+test('configure invalid URL template', () => {
+  const screen = render(<Demo />);
+
+  const urlTemplate = 'https://elastic.co/?{{event.wrongKey}}={{event.wrongValue}}';
+  fireEvent.change(screen.getByLabelText(/Enter URL template/i), {
+    target: { value: urlTemplate },
+  });
+
+  const previewTextArea = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement;
+  expect(previewTextArea.disabled).toEqual(true);
+  expect(previewTextArea.value).toEqual(urlTemplate);
+  expect(screen.getByText(/invalid format/i)).toBeInTheDocument(); // check that error is shown
+
+  const previewLink = screen.getByText('Preview') as HTMLAnchorElement;
+  expect(previewLink.href).toEqual(urlTemplate);
+  expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`);
+});
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx
new file mode 100644
index 000000000000..dabf09e4b6e9
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx
@@ -0,0 +1,226 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useRef, useState } from 'react';
+import {
+  EuiCheckbox,
+  EuiFormRow,
+  EuiIcon,
+  EuiLink,
+  EuiPopover,
+  EuiPopoverFooter,
+  EuiPopoverTitle,
+  EuiSelectable,
+  EuiText,
+  EuiTextArea,
+  EuiSelectableOption,
+} from '@elastic/eui';
+import { UrlDrilldownConfig, UrlDrilldownScope } from '../../types';
+import { compile } from '../../url_template';
+import { validateUrlTemplate } from '../../url_validation';
+import { buildScopeSuggestions } from '../../url_drilldown_scope';
+import './index.scss';
+import {
+  txtAddVariableButtonTitle,
+  txtUrlPreviewHelpText,
+  txtUrlTemplateSyntaxHelpLinkText,
+  txtUrlTemplateVariablesHelpLinkText,
+  txtUrlTemplateVariablesFilterPlaceholderText,
+  txtUrlTemplateLabel,
+  txtUrlTemplateOpenInNewTab,
+  txtUrlTemplatePlaceholder,
+  txtUrlTemplatePreviewLabel,
+  txtUrlTemplatePreviewLinkText,
+} from './i18n';
+
+export interface UrlDrilldownCollectConfig {
+  config: UrlDrilldownConfig;
+  onConfig: (newConfig: UrlDrilldownConfig) => void;
+  scope: UrlDrilldownScope;
+  syntaxHelpDocsLink?: string;
+}
+
+export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfig> = ({
+  config,
+  onConfig,
+  scope,
+  syntaxHelpDocsLink,
+}) => {
+  const textAreaRef = useRef<HTMLTextAreaElement>(null);
+  const urlTemplate = config.url.template ?? '';
+  const compiledUrl = React.useMemo(() => {
+    try {
+      return compile(urlTemplate, scope);
+    } catch {
+      return urlTemplate;
+    }
+  }, [urlTemplate, scope]);
+  const scopeVariables = React.useMemo(() => buildScopeSuggestions(scope), [scope]);
+
+  function updateUrlTemplate(newUrlTemplate: string) {
+    if (config.url.template !== newUrlTemplate) {
+      onConfig({
+        ...config,
+        url: {
+          ...config.url,
+          template: newUrlTemplate,
+        },
+      });
+    }
+  }
+  const { error, isValid } = React.useMemo(
+    () => validateUrlTemplate({ template: urlTemplate }, scope),
+    [urlTemplate, scope]
+  );
+  const isEmpty = !urlTemplate;
+  const isInvalid = !isValid && !isEmpty;
+  return (
+    <>
+      <EuiFormRow
+        fullWidth
+        isInvalid={isInvalid}
+        error={error}
+        className={'uaeUrlDrilldownCollectConfig__urlTemplateFormRow'}
+        label={txtUrlTemplateLabel}
+        helpText={
+          syntaxHelpDocsLink && (
+            <EuiLink external target={'_blank'} href={syntaxHelpDocsLink}>
+              {txtUrlTemplateSyntaxHelpLinkText}
+            </EuiLink>
+          )
+        }
+        labelAppend={
+          <AddVariableButton
+            variables={scopeVariables}
+            variablesHelpLink={syntaxHelpDocsLink}
+            onSelect={(variable: string) => {
+              if (textAreaRef.current) {
+                updateUrlTemplate(
+                  urlTemplate.substr(0, textAreaRef.current!.selectionStart) +
+                    `{{${variable}}}` +
+                    urlTemplate.substr(textAreaRef.current!.selectionEnd)
+                );
+              } else {
+                updateUrlTemplate(urlTemplate + `{{${variable}}}`);
+              }
+            }}
+          />
+        }
+      >
+        <EuiTextArea
+          fullWidth
+          isInvalid={isInvalid}
+          name="url"
+          data-test-subj="urlInput"
+          value={urlTemplate}
+          placeholder={txtUrlTemplatePlaceholder}
+          onChange={(event) => updateUrlTemplate(event.target.value)}
+          rows={3}
+          inputRef={textAreaRef}
+        />
+      </EuiFormRow>
+      <EuiFormRow
+        fullWidth
+        label={txtUrlTemplatePreviewLabel}
+        labelAppend={
+          <EuiText size="xs">
+            <EuiLink href={compiledUrl} target="_blank" external>
+              {txtUrlTemplatePreviewLinkText}
+            </EuiLink>
+          </EuiText>
+        }
+        helpText={txtUrlPreviewHelpText}
+      >
+        <EuiTextArea
+          fullWidth
+          name="urlPreview"
+          data-test-subj="urlPreview"
+          value={compiledUrl}
+          disabled={true}
+          rows={3}
+        />
+      </EuiFormRow>
+      <EuiFormRow hasChildLabel={false}>
+        <EuiCheckbox
+          id="openInNewTab"
+          name="openInNewTab"
+          label={txtUrlTemplateOpenInNewTab}
+          checked={config.openInNewTab}
+          onChange={() => onConfig({ ...config, openInNewTab: !config.openInNewTab })}
+        />
+      </EuiFormRow>
+    </>
+  );
+};
+
+function AddVariableButton({
+  variables,
+  onSelect,
+  variablesHelpLink,
+}: {
+  variables: string[];
+  onSelect: (variable: string) => void;
+  variablesHelpLink?: string;
+}) {
+  const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState<boolean>(false);
+  const closePopover = () => setIsVariablesPopoverOpen(false);
+
+  const options: EuiSelectableOption[] = variables.map((variable: string) => ({
+    key: variable,
+    label: variable,
+  }));
+
+  return (
+    <EuiPopover
+      ownFocus={true}
+      button={
+        <EuiText size="xs">
+          <EuiLink onClick={() => setIsVariablesPopoverOpen(true)}>
+            {txtAddVariableButtonTitle} <EuiIcon type="indexOpen" />
+          </EuiLink>
+        </EuiText>
+      }
+      isOpen={isVariablesPopoverOpen}
+      closePopover={closePopover}
+      panelPaddingSize="none"
+      anchorPosition="downLeft"
+      withTitle
+    >
+      <EuiSelectable
+        singleSelection={true}
+        searchable
+        searchProps={{
+          placeholder: txtUrlTemplateVariablesFilterPlaceholderText,
+          compressed: true,
+        }}
+        options={options}
+        onChange={(newOptions) => {
+          const selected = newOptions.find((o) => o.checked === 'on');
+          if (!selected) return;
+          onSelect(selected.key!);
+          closePopover();
+        }}
+        listProps={{
+          showIcons: false,
+        }}
+      >
+        {(list, search) => (
+          <div style={{ width: 320 }}>
+            <EuiPopoverTitle>{search}</EuiPopoverTitle>
+            {list}
+            {variablesHelpLink && (
+              <EuiPopoverFooter className={'eui-textRight'}>
+                <EuiLink external href={variablesHelpLink} target="_blank">
+                  {txtUrlTemplateVariablesHelpLinkText}
+                </EuiLink>
+              </EuiPopoverFooter>
+            )}
+          </div>
+        )}
+      </EuiSelectable>
+    </EuiPopover>
+  );
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts
new file mode 100644
index 000000000000..7b7a850050c4
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { UrlDrilldownConfig, UrlDrilldownGlobalScope, UrlDrilldownScope } from './types';
+export { UrlDrilldownCollectConfig } from './components';
+export {
+  validateUrlTemplate as urlDrilldownValidateUrlTemplate,
+  validateUrl as urlDrilldownValidateUrl,
+} from './url_validation';
+export { compile as urlDrilldownCompileUrl } from './url_template';
+export { globalScopeProvider as urlDrilldownGlobalScopeProvider } from './url_drilldown_global_scope';
+export {
+  buildScope as urlDrilldownBuildScope,
+  buildScopeSuggestions as urlDrilldownBuildScopeSuggestions,
+} from './url_drilldown_scope';
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts
new file mode 100644
index 000000000000..31c7481c9d63
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface UrlDrilldownConfig {
+  url: { format?: 'handlebars_v1'; template: string };
+  openInNewTab: boolean;
+}
+
+/**
+ * URL drilldown has 3 sources for variables: global, context and event variables
+ */
+export interface UrlDrilldownScope<
+  ContextScope extends object = object,
+  EventScope extends object = object
+> extends UrlDrilldownGlobalScope {
+  /**
+   * Dynamic variables that are differ depending on where drilldown is created and used,
+   * For example: variables extracted from embeddable panel
+   */
+  context?: ContextScope;
+
+  /**
+   * Variables extracted from trigger context
+   */
+  event?: EventScope;
+}
+
+/**
+ * Global static variables like, for example, `kibanaUrl`
+ * Such variables won’t change depending on a place where url drilldown is used.
+ */
+export interface UrlDrilldownGlobalScope {
+  kibanaUrl: string;
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts
new file mode 100644
index 000000000000..afc7fa590a2f
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CoreSetup } from 'kibana/public';
+import { UrlDrilldownGlobalScope } from './types';
+
+interface UrlDrilldownGlobalScopeDeps {
+  core: CoreSetup;
+}
+
+export function globalScopeProvider({
+  core,
+}: UrlDrilldownGlobalScopeDeps): () => UrlDrilldownGlobalScope {
+  return () => ({
+    kibanaUrl: window.location.origin + core.http.basePath.get(),
+  });
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts
new file mode 100644
index 000000000000..f95fc5e70ae0
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { buildScope, buildScopeSuggestions } from './url_drilldown_scope';
+
+test('buildScopeSuggestions', () => {
+  expect(
+    buildScopeSuggestions(
+      buildScope({
+        globalScope: {
+          kibanaUrl: 'http://localhost:5061/',
+        },
+        eventScope: {
+          key: '__testKey__',
+          value: '__testValue__',
+        },
+        contextScope: {
+          filters: [
+            {
+              query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
+              meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
+            },
+            {
+              query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
+              meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
+            },
+            {
+              query: { match: { _type: { query: 'nginx', type: 'phrase' } } },
+              meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
+            },
+          ],
+          query: {
+            query: '',
+            language: 'kquery',
+          },
+        },
+      })
+    )
+  ).toMatchInlineSnapshot(`
+    Array [
+      "event.key",
+      "event.value",
+      "context.filters",
+      "context.query.language",
+      "context.query.query",
+      "kibanaUrl",
+    ]
+  `);
+});
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts
new file mode 100644
index 000000000000..d499812a9d5a
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import partition from 'lodash/partition';
+import { UrlDrilldownGlobalScope, UrlDrilldownScope } from './types';
+import { getFlattenedObject } from '../../../../../../src/core/public';
+
+export function buildScope<
+  ContextScope extends object = object,
+  EventScope extends object = object
+>({
+  globalScope,
+  contextScope,
+  eventScope,
+}: {
+  globalScope: UrlDrilldownGlobalScope;
+  contextScope?: ContextScope;
+  eventScope?: EventScope;
+}): UrlDrilldownScope<ContextScope, EventScope> {
+  return {
+    ...globalScope,
+    context: contextScope,
+    event: eventScope,
+  };
+}
+
+/**
+ * Builds list of variables for suggestion from scope
+ * keys sorted alphabetically, except {{event.$}} variables are pulled to the top
+ * @param scope
+ */
+export function buildScopeSuggestions(scope: UrlDrilldownGlobalScope): string[] {
+  const allKeys = Object.keys(getFlattenedObject(scope)).sort();
+  const [eventKeys, otherKeys] = partition(allKeys, (key) => key.startsWith('event'));
+  return [...eventKeys, ...otherKeys];
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts
new file mode 100644
index 000000000000..64b8cc49292b
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts
@@ -0,0 +1,141 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { compile } from './url_template';
+import moment from 'moment-timezone';
+
+test('should compile url without variables', () => {
+  const url = 'https://elastic.co';
+  expect(compile(url, {})).toBe(url);
+});
+
+test('should fail on unknown syntax', () => {
+  const url = 'https://elastic.co/{{}';
+  expect(() => compile(url, {})).toThrowError();
+});
+
+test('should fail on not existing variable', () => {
+  const url = 'https://elastic.co/{{fake}}';
+  expect(() => compile(url, {})).toThrowError();
+});
+
+test('should fail on not existing nested variable', () => {
+  const url = 'https://elastic.co/{{fake.fake}}';
+  expect(() => compile(url, { fake: {} })).toThrowError();
+});
+
+test('should replace existing variable', () => {
+  const url = 'https://elastic.co/{{foo}}';
+  expect(compile(url, { foo: 'bar' })).toMatchInlineSnapshot(`"https://elastic.co/bar"`);
+});
+
+test('should fail on unknown helper', () => {
+  const url = 'https://elastic.co/{{fake foo}}';
+  expect(() => compile(url, { foo: 'bar' })).toThrowError();
+});
+
+describe('json helper', () => {
+  test('should replace with json', () => {
+    const url = 'https://elastic.co/{{json foo bar}}';
+    expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot(
+      `"https://elastic.co/%5B%7B%22foo%22:%22bar%22%7D,%7B%22bar%22:%22foo%22%7D%5D"`
+    );
+  });
+  test('should replace with json and skip encoding', () => {
+    const url = 'https://elastic.co/{{{json foo bar}}}';
+    expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot(
+      `"https://elastic.co/%5B%7B%22foo%22:%22bar%22%7D,%7B%22bar%22:%22foo%22%7D%5D"`
+    );
+  });
+  test('should throw on unknown key', () => {
+    const url = 'https://elastic.co/{{{json fake}}}';
+    expect(() => compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toThrowError();
+  });
+});
+
+describe('rison helper', () => {
+  test('should replace with rison', () => {
+    const url = 'https://elastic.co/{{rison foo bar}}';
+    expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot(
+      `"https://elastic.co/!((foo:bar),(bar:foo))"`
+    );
+  });
+  test('should replace with rison and skip encoding', () => {
+    const url = 'https://elastic.co/{{{rison foo bar}}}';
+    expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot(
+      `"https://elastic.co/!((foo:bar),(bar:foo))"`
+    );
+  });
+  test('should throw on unknown key', () => {
+    const url = 'https://elastic.co/{{{rison fake}}}';
+    expect(() => compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toThrowError();
+  });
+});
+
+describe('date helper', () => {
+  let spy: jest.SpyInstance;
+  const date = new Date('2020-08-18T14:45:00.000Z');
+  beforeAll(() => {
+    spy = jest.spyOn(global.Date, 'now').mockImplementation(() => date.valueOf());
+    moment.tz.setDefault('UTC');
+  });
+  afterAll(() => {
+    spy.mockRestore();
+    moment.tz.setDefault('Browser');
+  });
+
+  test('uses datemath', () => {
+    const url = 'https://elastic.co/{{date time}}';
+    expect(compile(url, { time: 'now' })).toMatchInlineSnapshot(
+      `"https://elastic.co/2020-08-18T14:45:00.000Z"`
+    );
+  });
+
+  test('can use format', () => {
+    const url = 'https://elastic.co/{{date time "dddd, MMMM Do YYYY, h:mm:ss a"}}';
+    expect(compile(url, { time: 'now' })).toMatchInlineSnapshot(
+      `"https://elastic.co/Tuesday,%20August%2018th%202020,%202:45:00%20pm"`
+    );
+  });
+
+  test('throws if missing variable', () => {
+    const url = 'https://elastic.co/{{date time}}';
+    expect(() => compile(url, {})).toThrowError();
+  });
+
+  test("doesn't throw if non valid date", () => {
+    const url = 'https://elastic.co/{{date time}}';
+    expect(compile(url, { time: 'fake' })).toMatchInlineSnapshot(`"https://elastic.co/fake"`);
+  });
+
+  test("doesn't throw on boolean or number", () => {
+    const url = 'https://elastic.co/{{date time}}';
+    expect(compile(url, { time: false })).toMatchInlineSnapshot(`"https://elastic.co/false"`);
+    expect(compile(url, { time: 24 })).toMatchInlineSnapshot(
+      `"https://elastic.co/1970-01-01T00:00:00.024Z"`
+    );
+  });
+
+  test('works with ISO string', () => {
+    const url = 'https://elastic.co/{{date time}}';
+    expect(compile(url, { time: date.toISOString() })).toMatchInlineSnapshot(
+      `"https://elastic.co/2020-08-18T14:45:00.000Z"`
+    );
+  });
+
+  test('works with ts', () => {
+    const url = 'https://elastic.co/{{date time}}';
+    expect(compile(url, { time: date.valueOf() })).toMatchInlineSnapshot(
+      `"https://elastic.co/2020-08-18T14:45:00.000Z"`
+    );
+  });
+  test('works with ts string', () => {
+    const url = 'https://elastic.co/{{date time}}';
+    expect(compile(url, { time: String(date.valueOf()) })).toMatchInlineSnapshot(
+      `"https://elastic.co/2020-08-18T14:45:00.000Z"`
+    );
+  });
+});
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts
new file mode 100644
index 000000000000..2c3537636b9d
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { create as createHandlebars, HelperDelegate, HelperOptions } from 'handlebars';
+import { encode, RisonValue } from 'rison-node';
+import dateMath from '@elastic/datemath';
+import moment, { Moment } from 'moment';
+
+const handlebars = createHandlebars();
+
+function createSerializationHelper(
+  fnName: string,
+  serializeFn: (value: unknown) => string
+): HelperDelegate {
+  return (...args) => {
+    const { hash } = args.slice(-1)[0] as HelperOptions;
+    const hasHash = Object.keys(hash).length > 0;
+    const hasValues = args.length > 1;
+    if (hasHash && hasValues) {
+      throw new Error(`[${fnName}]: both value list and hash are not supported`);
+    }
+    if (hasHash) {
+      if (Object.values(hash).some((v) => typeof v === 'undefined'))
+        throw new Error(`[${fnName}]: unknown variable`);
+      return serializeFn(hash);
+    } else {
+      const values = args.slice(0, -1) as unknown[];
+      if (values.some((value) => typeof value === 'undefined'))
+        throw new Error(`[${fnName}]: unknown variable`);
+      if (values.length === 0) throw new Error(`[${fnName}]: unknown variable`);
+      if (values.length === 1) return serializeFn(values[0]);
+      return serializeFn(values);
+    }
+  };
+}
+
+handlebars.registerHelper('json', createSerializationHelper('json', JSON.stringify));
+handlebars.registerHelper(
+  'rison',
+  createSerializationHelper('rison', (v) => encode(v as RisonValue))
+);
+
+handlebars.registerHelper('date', (...args) => {
+  const values = args.slice(0, -1) as [string | Date, string | undefined];
+  // eslint-disable-next-line prefer-const
+  let [date, format] = values;
+  if (typeof date === 'undefined') throw new Error(`[date]: unknown variable`);
+  let momentDate: Moment | undefined;
+  if (typeof date === 'string') {
+    momentDate = dateMath.parse(date);
+    if (!momentDate || !momentDate.isValid()) {
+      const ts = Number(date);
+      if (!Number.isNaN(ts)) {
+        momentDate = moment(ts);
+      }
+    }
+  } else {
+    momentDate = moment(date);
+  }
+
+  if (!momentDate || !momentDate.isValid()) {
+    // do not throw error here, because it could be that in preview `__testValue__` is not parsable,
+    // but in runtime it is
+    return date;
+  }
+  return format ? momentDate.format(format) : momentDate.toISOString();
+});
+
+export function compile(url: string, context: object): string {
+  const template = handlebars.compile(url, { strict: true, noEscape: true });
+  return encodeURI(template(context));
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts
new file mode 100644
index 000000000000..cb6f4a28402d
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { validateUrl, validateUrlTemplate } from './url_validation';
+
+describe('validateUrl', () => {
+  describe('unsafe urls', () => {
+    const unsafeUrls = [
+      // eslint-disable-next-line no-script-url
+      'javascript:evil()',
+      // eslint-disable-next-line no-script-url
+      'JavaScript:abc',
+      'evilNewProtocol:abc',
+      ' \n Java\n Script:abc',
+      '&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;',
+      '&#106&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;',
+      '&#106 &#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;',
+      '&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058',
+      '&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A;',
+      'jav&#x09;ascript:alert();',
+      // 'jav\u0000ascript:alert();', CI fails on this one
+      'data:;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
+      'data:,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
+      'data:iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
+      'data:text/javascript;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
+      'data:application/x-msdownload;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
+    ];
+
+    for (const url of unsafeUrls) {
+      test(`unsafe ${url}`, () => {
+        expect(validateUrl(url).isValid).toBe(false);
+      });
+    }
+  });
+
+  describe('invalid urls', () => {
+    const invalidUrls = ['elastic.co', 'www.elastic.co', 'test', '', ' ', 'https://'];
+    for (const url of invalidUrls) {
+      test(`invalid ${url}`, () => {
+        expect(validateUrl(url).isValid).toBe(false);
+      });
+    }
+  });
+
+  describe('valid urls', () => {
+    const validUrls = [
+      'https://elastic.co',
+      'https://www.elastic.co',
+      'http://elastic',
+      'mailto:someone',
+    ];
+    for (const url of validUrls) {
+      test(`valid ${url}`, () => {
+        expect(validateUrl(url).isValid).toBe(true);
+      });
+    }
+  });
+});
+
+describe('validateUrlTemplate', () => {
+  test('domain in variable is allowed', () => {
+    expect(
+      validateUrlTemplate(
+        { template: '{{kibanaUrl}}/test' },
+        { kibanaUrl: 'http://localhost:5601/app' }
+      ).isValid
+    ).toBe(true);
+  });
+
+  test('unsafe domain in variable is not allowed', () => {
+    expect(
+      // eslint-disable-next-line no-script-url
+      validateUrlTemplate({ template: '{{kibanaUrl}}/test' }, { kibanaUrl: 'javascript:evil()' })
+        .isValid
+    ).toBe(false);
+  });
+
+  test('if missing variable then invalid', () => {
+    expect(
+      validateUrlTemplate({ template: '{{url}}/test' }, { kibanaUrl: 'http://localhost:5601/app' })
+        .isValid
+    ).toBe(false);
+  });
+});
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts
new file mode 100644
index 000000000000..b32f5d84c677
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { UrlDrilldownConfig, UrlDrilldownScope } from './types';
+import { compile } from './url_template';
+
+const generalFormatError = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage',
+  {
+    defaultMessage: 'Invalid format. Example: {exampleUrl}',
+    values: {
+      exampleUrl: 'https://www.my-url.com/?{{event.key}}={{event.value}}',
+    },
+  }
+);
+
+const formatError = (message: string) =>
+  i18n.translate(
+    'xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage',
+    {
+      defaultMessage: 'Invalid format: {message}',
+      values: {
+        message,
+      },
+    }
+  );
+
+const SAFE_URL_PATTERN = /^(?:(?:https?|mailto):|[^&:/?#]*(?:[/?#]|$))/gi;
+export function validateUrl(url: string): { isValid: boolean; error?: string } {
+  if (!url)
+    return {
+      isValid: false,
+      error: generalFormatError,
+    };
+
+  try {
+    new URL(url);
+    if (!url.match(SAFE_URL_PATTERN)) throw new Error();
+    return { isValid: true };
+  } catch (e) {
+    return {
+      isValid: false,
+      error: generalFormatError,
+    };
+  }
+}
+
+export function validateUrlTemplate(
+  urlTemplate: UrlDrilldownConfig['url'],
+  scope: UrlDrilldownScope
+): { isValid: boolean; error?: string } {
+  if (!urlTemplate.template)
+    return {
+      isValid: false,
+      error: generalFormatError,
+    };
+
+  try {
+    const compiledUrl = compile(urlTemplate.template, scope);
+    return validateUrl(compiledUrl);
+  } catch (e) {
+    return {
+      isValid: false,
+      error: formatError(e.message),
+    };
+  }
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/index.ts b/x-pack/plugins/ui_actions_enhanced/public/index.ts
index a255bc28f5c6..4a899b24852a 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/index.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/index.ts
@@ -32,3 +32,4 @@ export {
 } from './dynamic_actions';
 
 export { DrilldownDefinition as UiActionsEnhancedDrilldownDefinition } from './drilldowns';
+export * from './drilldowns/url_drilldown';
diff --git a/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js b/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js
index 1e3ab0d96b81..bf43167a3ae5 100644
--- a/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js
+++ b/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js
@@ -9,8 +9,5 @@ import { join } from 'path';
 // eslint-disable-next-line
 require('@kbn/storybook').runStorybookCli({
   name: 'ui_actions_enhanced',
-  storyGlobs: [
-    join(__dirname, '..', 'public', 'components', '**', '*.story.tsx'),
-    join(__dirname, '..', 'public', 'drilldowns', 'components', '**', '*.story.tsx'),
-  ],
+  storyGlobs: [join(__dirname, '..', 'public', '**', '*.story.tsx')],
 });
diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts b/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts
index 2a8cde85ee3c..e3d68ef69035 100644
--- a/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts
+++ b/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts
@@ -18,7 +18,6 @@ export default function ({ getService }: FtrProviderContext) {
         .put('/api/logstash/pipeline/fast_generator')
         .set('kbn-xsrf', 'xxx')
         .send({
-          id: 'fast_generator',
           description: 'foobar baz',
           pipeline: 'input { generator {} }\n\n output { stdout {} }',
         })
diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/save.ts b/x-pack/test/api_integration/apis/logstash/pipeline/save.ts
index f44c5e6252d5..1c2f23e81eaf 100644
--- a/x-pack/test/api_integration/apis/logstash/pipeline/save.ts
+++ b/x-pack/test/api_integration/apis/logstash/pipeline/save.ts
@@ -26,7 +26,6 @@ export default function ({ getService }: FtrProviderContext) {
         .put('/api/logstash/pipeline/fast_generator')
         .set('kbn-xsrf', 'xxx')
         .send({
-          id: 'fast_generator',
           description: 'foobar baz',
           pipeline: 'input { generator {} }\n\n output { stdout {} }',
         })
diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts
similarity index 99%
rename from x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts
rename to x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts
index 29ead0db1c63..c300412c393b 100644
--- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts
+++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts
@@ -22,7 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
   const testSubjects = getService('testSubjects');
   const filterBar = getService('filterBar');
 
-  describe('Dashboard Drilldowns', function () {
+  describe('Dashboard to dashboard drilldown', function () {
     before(async () => {
       log.debug('Dashboard Drilldowns:initTests');
       await PageObjects.common.navigateToApp('dashboard');
diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts
new file mode 100644
index 000000000000..12de29c4fde1
--- /dev/null
+++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts
@@ -0,0 +1,96 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+const DRILLDOWN_TO_DISCOVER_URL = 'Go to discover';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+  const dashboardPanelActions = getService('dashboardPanelActions');
+  const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions');
+  const dashboardDrilldownsManage = getService('dashboardDrilldownsManage');
+  const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker', 'discover']);
+  const log = getService('log');
+  const browser = getService('browser');
+  const testSubjects = getService('testSubjects');
+
+  describe('Dashboard to URL drilldown', function () {
+    before(async () => {
+      log.debug('Dashboard to URL:initTests');
+      await PageObjects.common.navigateToApp('dashboard');
+      await PageObjects.dashboard.preserveCrossAppState();
+    });
+
+    it('should create dashboard to URL drilldown and use it to navigate to discover', async () => {
+      await PageObjects.dashboard.gotoDashboardEditMode(
+        dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME
+      );
+
+      // create drilldown
+      await dashboardPanelActions.openContextMenu();
+      await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction();
+      await dashboardDrilldownPanelActions.clickCreateDrilldown();
+      await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen();
+
+      const urlTemplate = `{{kibanaUrl}}/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'{{event.from}}',to:'{{event.to}}'))&_a=(columns:!(_source),filters:{{rison context.panel.filters}},index:'{{context.panel.indexPatternId}}',interval:auto,query:(language:{{context.panel.query.language}},query:'{{context.panel.query.query}}'),sort:!())`;
+
+      await dashboardDrilldownsManage.fillInDashboardToURLDrilldownWizard({
+        drilldownName: DRILLDOWN_TO_DISCOVER_URL,
+        destinationURLTemplate: urlTemplate,
+        trigger: 'SELECT_RANGE_TRIGGER',
+      });
+      await dashboardDrilldownsManage.saveChanges();
+      await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose();
+
+      // check that drilldown notification badge is shown
+      expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(2);
+
+      // save dashboard, navigate to view mode
+      await PageObjects.dashboard.saveDashboard(
+        dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME,
+        {
+          saveAsNew: false,
+          waitDialogIsClosed: true,
+        }
+      );
+
+      const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
+
+      await brushAreaChart();
+      await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened();
+      await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_DISCOVER_URL);
+
+      await PageObjects.discover.waitForDiscoverAppOnScreen();
+
+      // check that new time range duration was applied
+      const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
+      expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours);
+    });
+  });
+
+  // utils which shouldn't be a part of test flow, but also too specific to be moved to pageobject or service
+  async function brushAreaChart() {
+    const areaChart = await testSubjects.find('visualizationLoader');
+    expect(await areaChart.getAttribute('data-title')).to.be('Visualization漢字 AreaChart');
+    await browser.dragAndDrop(
+      {
+        location: areaChart,
+        offset: {
+          x: -100,
+          y: 0,
+        },
+      },
+      {
+        location: areaChart,
+        offset: {
+          x: 100,
+          y: 0,
+        },
+      }
+    );
+  }
+}
diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts
index ff604b18e1d5..57454f50266d 100644
--- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts
+++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts
@@ -22,7 +22,8 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) {
       await esArchiver.unload('dashboard/drilldowns');
     });
 
-    loadTestFile(require.resolve('./dashboard_drilldowns'));
+    loadTestFile(require.resolve('./dashboard_to_dashboard_drilldown'));
+    loadTestFile(require.resolve('./dashboard_to_url_drilldown'));
     loadTestFile(require.resolve('./explore_data_panel_action'));
 
     // Disabled for now as it requires xpack.discoverEnhanced.actions.exploreDataInChart.enabled
diff --git a/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz b/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz
new file mode 100644
index 000000000000..e1b9c01101f6
Binary files /dev/null and b/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz differ
diff --git a/x-pack/test/functional/es_archives/endpoint/resolver/signals/mappings.json b/x-pack/test/functional/es_archives/endpoint/resolver/signals/mappings.json
new file mode 100644
index 000000000000..ad77961a4144
--- /dev/null
+++ b/x-pack/test/functional/es_archives/endpoint/resolver/signals/mappings.json
@@ -0,0 +1,3239 @@
+{
+  "type": "index",
+  "value": {
+    "aliases": {
+      ".siem-signals-default": {
+        "is_write_index": true
+      }
+    },
+    "index": ".siem-signals-default-000001",
+    "mappings": {
+      "dynamic": "false",
+      "properties": {
+        "@timestamp": {
+          "type": "date"
+        },
+        "agent": {
+          "properties": {
+            "ephemeral_id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "as": {
+          "properties": {
+            "number": {
+              "type": "long"
+            },
+            "organization": {
+              "properties": {
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+        },
+        "client": {
+          "properties": {
+            "address": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "as": {
+              "properties": {
+                "number": {
+                  "type": "long"
+                },
+                "organization": {
+                  "properties": {
+                    "name": {
+                      "fields": {
+                        "text": {
+                          "norms": false,
+                          "type": "text"
+                        }
+                      },
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                }
+              }
+            },
+            "bytes": {
+              "type": "long"
+            },
+            "domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "geo": {
+              "properties": {
+                "city_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "continent_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "location": {
+                  "type": "geo_point"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "ip": {
+              "type": "ip"
+            },
+            "mac": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "nat": {
+              "properties": {
+                "ip": {
+                  "type": "ip"
+                },
+                "port": {
+                  "type": "long"
+                }
+              }
+            },
+            "packets": {
+              "type": "long"
+            },
+            "port": {
+              "type": "long"
+            },
+            "registered_domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "top_level_domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "user": {
+              "properties": {
+                "domain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "email": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "full_name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "group": {
+                  "properties": {
+                    "domain": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "id": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "hash": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+        },
+        "cloud": {
+          "properties": {
+            "account": {
+              "properties": {
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "availability_zone": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "instance": {
+              "properties": {
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "machine": {
+              "properties": {
+                "type": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "provider": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "region": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "code_signature": {
+          "properties": {
+            "exists": {
+              "type": "boolean"
+            },
+            "status": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "subject_name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "trusted": {
+              "type": "boolean"
+            },
+            "valid": {
+              "type": "boolean"
+            }
+          }
+        },
+        "container": {
+          "properties": {
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "image": {
+              "properties": {
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "tag": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "labels": {
+              "type": "object"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "runtime": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "destination": {
+          "properties": {
+            "address": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "as": {
+              "properties": {
+                "number": {
+                  "type": "long"
+                },
+                "organization": {
+                  "properties": {
+                    "name": {
+                      "fields": {
+                        "text": {
+                          "norms": false,
+                          "type": "text"
+                        }
+                      },
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                }
+              }
+            },
+            "bytes": {
+              "type": "long"
+            },
+            "domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "geo": {
+              "properties": {
+                "city_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "continent_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "location": {
+                  "type": "geo_point"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "ip": {
+              "type": "ip"
+            },
+            "mac": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "nat": {
+              "properties": {
+                "ip": {
+                  "type": "ip"
+                },
+                "port": {
+                  "type": "long"
+                }
+              }
+            },
+            "packets": {
+              "type": "long"
+            },
+            "port": {
+              "type": "long"
+            },
+            "registered_domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "top_level_domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "user": {
+              "properties": {
+                "domain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "email": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "full_name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "group": {
+                  "properties": {
+                    "domain": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "id": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "hash": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+        },
+        "dll": {
+          "properties": {
+            "code_signature": {
+              "properties": {
+                "exists": {
+                  "type": "boolean"
+                },
+                "status": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "subject_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "trusted": {
+                  "type": "boolean"
+                },
+                "valid": {
+                  "type": "boolean"
+                }
+              }
+            },
+            "hash": {
+              "properties": {
+                "md5": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "sha1": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "sha256": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "sha512": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "path": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "pe": {
+              "properties": {
+                "company": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "description": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "file_version": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "original_file_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "product": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+        },
+        "dns": {
+          "properties": {
+            "answers": {
+              "properties": {
+                "class": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "data": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "ttl": {
+                  "type": "long"
+                },
+                "type": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "header_flags": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "op_code": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "question": {
+              "properties": {
+                "class": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "registered_domain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "subdomain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "top_level_domain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "type": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "resolved_ip": {
+              "type": "ip"
+            },
+            "response_code": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "ecs": {
+          "properties": {
+            "version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "error": {
+          "properties": {
+            "code": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "message": {
+              "norms": false,
+              "type": "text"
+            },
+            "stack_trace": {
+              "doc_values": false,
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "index": false,
+              "type": "keyword"
+            },
+            "type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "event": {
+          "properties": {
+            "action": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "category": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "code": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "created": {
+              "type": "date"
+            },
+            "dataset": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "duration": {
+              "type": "long"
+            },
+            "end": {
+              "type": "date"
+            },
+            "hash": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "ingested": {
+              "type": "date"
+            },
+            "kind": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "module": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "original": {
+              "doc_values": false,
+              "ignore_above": 1024,
+              "index": false,
+              "type": "keyword"
+            },
+            "outcome": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "provider": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "reference": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "risk_score": {
+              "type": "float"
+            },
+            "risk_score_norm": {
+              "type": "float"
+            },
+            "sequence": {
+              "type": "long"
+            },
+            "severity": {
+              "type": "long"
+            },
+            "start": {
+              "type": "date"
+            },
+            "timezone": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "url": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "file": {
+          "properties": {
+            "accessed": {
+              "type": "date"
+            },
+            "attributes": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "code_signature": {
+              "properties": {
+                "exists": {
+                  "type": "boolean"
+                },
+                "status": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "subject_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "trusted": {
+                  "type": "boolean"
+                },
+                "valid": {
+                  "type": "boolean"
+                }
+              }
+            },
+            "created": {
+              "type": "date"
+            },
+            "ctime": {
+              "type": "date"
+            },
+            "device": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "directory": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "drive_letter": {
+              "ignore_above": 1,
+              "type": "keyword"
+            },
+            "extension": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "gid": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "group": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "hash": {
+              "properties": {
+                "md5": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "sha1": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "sha256": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "sha512": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "inode": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "mime_type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "mode": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "mtime": {
+              "type": "date"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "owner": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "path": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "pe": {
+              "properties": {
+                "company": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "description": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "file_version": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "original_file_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "product": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "size": {
+              "type": "long"
+            },
+            "target_path": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "uid": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "geo": {
+          "properties": {
+            "city_name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "continent_name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "country_iso_code": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "country_name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "location": {
+              "type": "geo_point"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "region_iso_code": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "region_name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "group": {
+          "properties": {
+            "domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "hash": {
+          "properties": {
+            "md5": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "sha1": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "sha256": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "sha512": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "host": {
+          "properties": {
+            "architecture": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "geo": {
+              "properties": {
+                "city_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "continent_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "location": {
+                  "type": "geo_point"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "hostname": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "ip": {
+              "type": "ip"
+            },
+            "mac": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "os": {
+              "properties": {
+                "family": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "full": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "kernel": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "platform": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "version": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "uptime": {
+              "type": "long"
+            },
+            "user": {
+              "properties": {
+                "domain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "email": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "full_name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "group": {
+                  "properties": {
+                    "domain": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "id": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "hash": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+        },
+        "http": {
+          "properties": {
+            "request": {
+              "properties": {
+                "body": {
+                  "properties": {
+                    "bytes": {
+                      "type": "long"
+                    },
+                    "content": {
+                      "fields": {
+                        "text": {
+                          "norms": false,
+                          "type": "text"
+                        }
+                      },
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "bytes": {
+                  "type": "long"
+                },
+                "method": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "referrer": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "response": {
+              "properties": {
+                "body": {
+                  "properties": {
+                    "bytes": {
+                      "type": "long"
+                    },
+                    "content": {
+                      "fields": {
+                        "text": {
+                          "norms": false,
+                          "type": "text"
+                        }
+                      },
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "bytes": {
+                  "type": "long"
+                },
+                "status_code": {
+                  "type": "long"
+                }
+              }
+            },
+            "version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "interface": {
+          "properties": {
+            "alias": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "labels": {
+          "type": "object"
+        },
+        "log": {
+          "properties": {
+            "level": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "logger": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "origin": {
+              "properties": {
+                "file": {
+                  "properties": {
+                    "line": {
+                      "type": "integer"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "function": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "original": {
+              "doc_values": false,
+              "ignore_above": 1024,
+              "index": false,
+              "type": "keyword"
+            },
+            "syslog": {
+              "properties": {
+                "facility": {
+                  "properties": {
+                    "code": {
+                      "type": "long"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "priority": {
+                  "type": "long"
+                },
+                "severity": {
+                  "properties": {
+                    "code": {
+                      "type": "long"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "message": {
+          "norms": false,
+          "type": "text"
+        },
+        "network": {
+          "properties": {
+            "application": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "bytes": {
+              "type": "long"
+            },
+            "community_id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "direction": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "forwarded_ip": {
+              "type": "ip"
+            },
+            "iana_number": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "inner": {
+              "properties": {
+                "vlan": {
+                  "properties": {
+                    "id": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                }
+              }
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "packets": {
+              "type": "long"
+            },
+            "protocol": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "transport": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "vlan": {
+              "properties": {
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+        },
+        "observer": {
+          "properties": {
+            "egress": {
+              "properties": {
+                "interface": {
+                  "properties": {
+                    "alias": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "id": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "vlan": {
+                  "properties": {
+                    "id": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "zone": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "geo": {
+              "properties": {
+                "city_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "continent_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "location": {
+                  "type": "geo_point"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "hostname": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "ingress": {
+              "properties": {
+                "interface": {
+                  "properties": {
+                    "alias": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "id": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "vlan": {
+                  "properties": {
+                    "id": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "zone": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "ip": {
+              "type": "ip"
+            },
+            "mac": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "os": {
+              "properties": {
+                "family": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "full": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "kernel": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "platform": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "version": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "product": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "serial_number": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "vendor": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "organization": {
+          "properties": {
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "os": {
+          "properties": {
+            "family": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "full": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "kernel": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "platform": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "package": {
+          "properties": {
+            "architecture": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "build_version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "checksum": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "description": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "install_scope": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "installed": {
+              "type": "date"
+            },
+            "license": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "path": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "reference": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "size": {
+              "type": "long"
+            },
+            "type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "pe": {
+          "properties": {
+            "company": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "description": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "file_version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "original_file_name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "product": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "process": {
+          "properties": {
+            "args": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "args_count": {
+              "type": "long"
+            },
+            "code_signature": {
+              "properties": {
+                "exists": {
+                  "type": "boolean"
+                },
+                "status": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "subject_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "trusted": {
+                  "type": "boolean"
+                },
+                "valid": {
+                  "type": "boolean"
+                }
+              }
+            },
+            "command_line": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "executable": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "exit_code": {
+              "type": "long"
+            },
+            "hash": {
+              "properties": {
+                "md5": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "sha1": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "sha256": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "sha512": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "name": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "parent": {
+              "properties": {
+                "args": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "args_count": {
+                  "type": "long"
+                },
+                "code_signature": {
+                  "properties": {
+                    "exists": {
+                      "type": "boolean"
+                    },
+                    "status": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "subject_name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "trusted": {
+                      "type": "boolean"
+                    },
+                    "valid": {
+                      "type": "boolean"
+                    }
+                  }
+                },
+                "command_line": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "executable": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "exit_code": {
+                  "type": "long"
+                },
+                "hash": {
+                  "properties": {
+                    "md5": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "sha1": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "sha256": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "sha512": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "pgid": {
+                  "type": "long"
+                },
+                "pid": {
+                  "type": "long"
+                },
+                "ppid": {
+                  "type": "long"
+                },
+                "start": {
+                  "type": "date"
+                },
+                "thread": {
+                  "properties": {
+                    "id": {
+                      "type": "long"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "title": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "uptime": {
+                  "type": "long"
+                },
+                "working_directory": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "pe": {
+              "properties": {
+                "company": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "description": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "file_version": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "original_file_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "product": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "pgid": {
+              "type": "long"
+            },
+            "pid": {
+              "type": "long"
+            },
+            "ppid": {
+              "type": "long"
+            },
+            "start": {
+              "type": "date"
+            },
+            "thread": {
+              "properties": {
+                "id": {
+                  "type": "long"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "title": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "uptime": {
+              "type": "long"
+            },
+            "working_directory": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "registry": {
+          "properties": {
+            "data": {
+              "properties": {
+                "bytes": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "strings": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "type": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "hive": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "key": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "path": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "value": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "related": {
+          "properties": {
+            "hash": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "ip": {
+              "type": "ip"
+            },
+            "user": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "rule": {
+          "properties": {
+            "author": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "category": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "description": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "license": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "reference": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "ruleset": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "uuid": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "server": {
+          "properties": {
+            "address": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "as": {
+              "properties": {
+                "number": {
+                  "type": "long"
+                },
+                "organization": {
+                  "properties": {
+                    "name": {
+                      "fields": {
+                        "text": {
+                          "norms": false,
+                          "type": "text"
+                        }
+                      },
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                }
+              }
+            },
+            "bytes": {
+              "type": "long"
+            },
+            "domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "geo": {
+              "properties": {
+                "city_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "continent_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "location": {
+                  "type": "geo_point"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "ip": {
+              "type": "ip"
+            },
+            "mac": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "nat": {
+              "properties": {
+                "ip": {
+                  "type": "ip"
+                },
+                "port": {
+                  "type": "long"
+                }
+              }
+            },
+            "packets": {
+              "type": "long"
+            },
+            "port": {
+              "type": "long"
+            },
+            "registered_domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "top_level_domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "user": {
+              "properties": {
+                "domain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "email": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "full_name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "group": {
+                  "properties": {
+                    "domain": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "id": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "hash": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+        },
+        "service": {
+          "properties": {
+            "ephemeral_id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "node": {
+              "properties": {
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "state": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "type": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "signal": {
+          "properties": {
+            "ancestors": {
+              "properties": {
+                "depth": {
+                  "type": "long"
+                },
+                "id": {
+                  "type": "keyword"
+                },
+                "rule": {
+                  "type": "keyword"
+                },
+                "type": {
+                  "type": "keyword"
+                }
+              }
+            },
+            "original_event": {
+              "properties": {
+                "action": {
+                  "type": "keyword"
+                },
+                "category": {
+                  "type": "keyword"
+                },
+                "code": {
+                  "type": "keyword"
+                },
+                "created": {
+                  "type": "date"
+                },
+                "dataset": {
+                  "type": "keyword"
+                },
+                "duration": {
+                  "type": "long"
+                },
+                "end": {
+                  "type": "date"
+                },
+                "hash": {
+                  "type": "keyword"
+                },
+                "id": {
+                  "type": "keyword"
+                },
+                "kind": {
+                  "type": "keyword"
+                },
+                "module": {
+                  "type": "keyword"
+                },
+                "original": {
+                  "doc_values": false,
+                  "index": false,
+                  "type": "keyword"
+                },
+                "outcome": {
+                  "type": "keyword"
+                },
+                "provider": {
+                  "type": "keyword"
+                },
+                "risk_score": {
+                  "type": "float"
+                },
+                "risk_score_norm": {
+                  "type": "float"
+                },
+                "sequence": {
+                  "type": "long"
+                },
+                "severity": {
+                  "type": "long"
+                },
+                "start": {
+                  "type": "date"
+                },
+                "timezone": {
+                  "type": "keyword"
+                },
+                "type": {
+                  "type": "keyword"
+                }
+              }
+            },
+            "original_time": {
+              "type": "date"
+            },
+            "parent": {
+              "properties": {
+                "depth": {
+                  "type": "long"
+                },
+                "id": {
+                  "type": "keyword"
+                },
+                "index": {
+                  "type": "keyword"
+                },
+                "rule": {
+                  "type": "keyword"
+                },
+                "type": {
+                  "type": "keyword"
+                }
+              }
+            },
+            "rule": {
+              "properties": {
+                "author": {
+                  "type": "keyword"
+                },
+                "building_block_type": {
+                  "type": "keyword"
+                },
+                "created_at": {
+                  "type": "date"
+                },
+                "created_by": {
+                  "type": "keyword"
+                },
+                "description": {
+                  "type": "keyword"
+                },
+                "enabled": {
+                  "type": "keyword"
+                },
+                "false_positives": {
+                  "type": "keyword"
+                },
+                "filters": {
+                  "type": "object"
+                },
+                "from": {
+                  "type": "keyword"
+                },
+                "id": {
+                  "type": "keyword"
+                },
+                "immutable": {
+                  "type": "keyword"
+                },
+                "index": {
+                  "type": "keyword"
+                },
+                "interval": {
+                  "type": "keyword"
+                },
+                "language": {
+                  "type": "keyword"
+                },
+                "license": {
+                  "type": "keyword"
+                },
+                "max_signals": {
+                  "type": "keyword"
+                },
+                "name": {
+                  "type": "keyword"
+                },
+                "note": {
+                  "type": "text"
+                },
+                "output_index": {
+                  "type": "keyword"
+                },
+                "query": {
+                  "type": "keyword"
+                },
+                "references": {
+                  "type": "keyword"
+                },
+                "risk_score": {
+                  "type": "float"
+                },
+                "risk_score_mapping": {
+                  "properties": {
+                    "field": {
+                      "type": "keyword"
+                    },
+                    "operator": {
+                      "type": "keyword"
+                    },
+                    "value": {
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "rule_id": {
+                  "type": "keyword"
+                },
+                "rule_name_override": {
+                  "type": "keyword"
+                },
+                "saved_id": {
+                  "type": "keyword"
+                },
+                "severity": {
+                  "type": "keyword"
+                },
+                "severity_mapping": {
+                  "properties": {
+                    "field": {
+                      "type": "keyword"
+                    },
+                    "operator": {
+                      "type": "keyword"
+                    },
+                    "severity": {
+                      "type": "keyword"
+                    },
+                    "value": {
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "size": {
+                  "type": "keyword"
+                },
+                "tags": {
+                  "type": "keyword"
+                },
+                "threat": {
+                  "properties": {
+                    "framework": {
+                      "type": "keyword"
+                    },
+                    "tactic": {
+                      "properties": {
+                        "id": {
+                          "type": "keyword"
+                        },
+                        "name": {
+                          "type": "keyword"
+                        },
+                        "reference": {
+                          "type": "keyword"
+                        }
+                      }
+                    },
+                    "technique": {
+                      "properties": {
+                        "id": {
+                          "type": "keyword"
+                        },
+                        "name": {
+                          "type": "keyword"
+                        },
+                        "reference": {
+                          "type": "keyword"
+                        }
+                      }
+                    }
+                  }
+                },
+                "threshold": {
+                  "properties": {
+                    "field": {
+                      "type": "keyword"
+                    },
+                    "value": {
+                      "type": "float"
+                    }
+                  }
+                },
+                "timeline_id": {
+                  "type": "keyword"
+                },
+                "timeline_title": {
+                  "type": "keyword"
+                },
+                "timestamp_override": {
+                  "type": "keyword"
+                },
+                "to": {
+                  "type": "keyword"
+                },
+                "type": {
+                  "type": "keyword"
+                },
+                "updated_at": {
+                  "type": "date"
+                },
+                "updated_by": {
+                  "type": "keyword"
+                },
+                "version": {
+                  "type": "keyword"
+                }
+              }
+            },
+            "status": {
+              "type": "keyword"
+            },
+            "threshold_count": {
+              "type": "float"
+            }
+          }
+        },
+        "source": {
+          "properties": {
+            "address": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "as": {
+              "properties": {
+                "number": {
+                  "type": "long"
+                },
+                "organization": {
+                  "properties": {
+                    "name": {
+                      "fields": {
+                        "text": {
+                          "norms": false,
+                          "type": "text"
+                        }
+                      },
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                }
+              }
+            },
+            "bytes": {
+              "type": "long"
+            },
+            "domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "geo": {
+              "properties": {
+                "city_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "continent_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "country_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "location": {
+                  "type": "geo_point"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_iso_code": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "region_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "ip": {
+              "type": "ip"
+            },
+            "mac": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "nat": {
+              "properties": {
+                "ip": {
+                  "type": "ip"
+                },
+                "port": {
+                  "type": "long"
+                }
+              }
+            },
+            "packets": {
+              "type": "long"
+            },
+            "port": {
+              "type": "long"
+            },
+            "registered_domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "top_level_domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "user": {
+              "properties": {
+                "domain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "email": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "full_name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "group": {
+                  "properties": {
+                    "domain": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "id": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "name": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "hash": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+        },
+        "tags": {
+          "ignore_above": 1024,
+          "type": "keyword"
+        },
+        "threat": {
+          "properties": {
+            "framework": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "tactic": {
+              "properties": {
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "reference": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "technique": {
+              "properties": {
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "reference": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            }
+          }
+        },
+        "tls": {
+          "properties": {
+            "cipher": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "client": {
+              "properties": {
+                "certificate": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "certificate_chain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "hash": {
+                  "properties": {
+                    "md5": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "sha1": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "sha256": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "issuer": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "ja3": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "not_after": {
+                  "type": "date"
+                },
+                "not_before": {
+                  "type": "date"
+                },
+                "server_name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "subject": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "supported_ciphers": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "curve": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "established": {
+              "type": "boolean"
+            },
+            "next_protocol": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "resumed": {
+              "type": "boolean"
+            },
+            "server": {
+              "properties": {
+                "certificate": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "certificate_chain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "hash": {
+                  "properties": {
+                    "md5": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "sha1": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    },
+                    "sha256": {
+                      "ignore_above": 1024,
+                      "type": "keyword"
+                    }
+                  }
+                },
+                "issuer": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "ja3s": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "not_after": {
+                  "type": "date"
+                },
+                "not_before": {
+                  "type": "date"
+                },
+                "subject": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "version_protocol": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "trace": {
+          "properties": {
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "transaction": {
+          "properties": {
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "url": {
+          "properties": {
+            "domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "extension": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "fragment": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "full": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "original": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "password": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "path": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "port": {
+              "type": "long"
+            },
+            "query": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "registered_domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "scheme": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "top_level_domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "username": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "user": {
+          "properties": {
+            "domain": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "email": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "full_name": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "group": {
+              "properties": {
+                "domain": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "id": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "hash": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "user_agent": {
+          "properties": {
+            "device": {
+              "properties": {
+                "name": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "original": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "os": {
+              "properties": {
+                "family": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "full": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "kernel": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "name": {
+                  "fields": {
+                    "text": {
+                      "norms": false,
+                      "type": "text"
+                    }
+                  },
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "platform": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                },
+                "version": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "version": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "vlan": {
+          "properties": {
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "name": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        },
+        "vulnerability": {
+          "properties": {
+            "category": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "classification": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "description": {
+              "fields": {
+                "text": {
+                  "norms": false,
+                  "type": "text"
+                }
+              },
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "enumeration": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "reference": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "report_id": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            },
+            "scanner": {
+              "properties": {
+                "vendor": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "score": {
+              "properties": {
+                "base": {
+                  "type": "float"
+                },
+                "environmental": {
+                  "type": "float"
+                },
+                "temporal": {
+                  "type": "float"
+                },
+                "version": {
+                  "ignore_above": 1024,
+                  "type": "keyword"
+                }
+              }
+            },
+            "severity": {
+              "ignore_above": 1024,
+              "type": "keyword"
+            }
+          }
+        }
+      }
+    },
+    "settings": {
+      "index": {
+        "lifecycle": {
+          "name": ".siem-signals-default",
+          "rollover_alias": ".siem-signals-default"
+        },
+        "mapping": {
+          "total_fields": {
+            "limit": "10000"
+          }
+        },
+        "number_of_replicas": "1",
+        "number_of_shards": "1",
+        "routing": {
+          "allocation": {
+            "include": {
+              "_tier": "data_hot"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/x-pack/test/functional/services/dashboard/drilldowns_manage.ts b/x-pack/test/functional/services/dashboard/drilldowns_manage.ts
index a01fde3a5233..7b66591fcf76 100644
--- a/x-pack/test/functional/services/dashboard/drilldowns_manage.ts
+++ b/x-pack/test/functional/services/dashboard/drilldowns_manage.ts
@@ -12,6 +12,8 @@ const DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM =
   'actionFactoryItem-DASHBOARD_TO_DASHBOARD_DRILLDOWN';
 const DASHBOARD_TO_DASHBOARD_ACTION_WIZARD =
   'selectedActionFactory-DASHBOARD_TO_DASHBOARD_DRILLDOWN';
+const DASHBOARD_TO_URL_ACTION_LIST_ITEM = 'actionFactoryItem-URL_DRILLDOWN';
+const DASHBOARD_TO_URL_ACTION_WIZARD = 'selectedActionFactory-URL_DRILLDOWN';
 const DESTINATION_DASHBOARD_SELECT = 'dashboardDrilldownSelectDashboard';
 const DRILLDOWN_WIZARD_SUBMIT = 'drilldownWizardSubmit';
 
@@ -68,10 +70,32 @@ export function DashboardDrilldownsManageProvider({ getService }: FtrProviderCon
       await this.selectDestinationDashboard(destinationDashboardTitle);
     }
 
+    async fillInDashboardToURLDrilldownWizard({
+      drilldownName,
+      destinationURLTemplate,
+      trigger,
+    }: {
+      drilldownName: string;
+      destinationURLTemplate: string;
+      trigger: 'VALUE_CLICK_TRIGGER' | 'SELECT_RANGE_TRIGGER';
+    }) {
+      await this.fillInDrilldownName(drilldownName);
+      await this.selectDashboardToURLActionIfNeeded();
+      await this.selectTriggerIfNeeded(trigger);
+      await this.fillInURLTemplate(destinationURLTemplate);
+    }
+
     async fillInDrilldownName(name: string) {
       await testSubjects.setValue('drilldownNameInput', name);
     }
 
+    async selectDashboardToURLActionIfNeeded() {
+      if (await testSubjects.exists(DASHBOARD_TO_URL_ACTION_LIST_ITEM)) {
+        await testSubjects.click(DASHBOARD_TO_URL_ACTION_LIST_ITEM);
+      }
+      await testSubjects.existOrFail(DASHBOARD_TO_URL_ACTION_WIZARD);
+    }
+
     async selectDashboardToDashboardActionIfNeeded() {
       if (await testSubjects.exists(DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM)) {
         await testSubjects.click(DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM);
@@ -83,6 +107,18 @@ export function DashboardDrilldownsManageProvider({ getService }: FtrProviderCon
       await comboBox.set(DESTINATION_DASHBOARD_SELECT, title);
     }
 
+    async selectTriggerIfNeeded(trigger: 'VALUE_CLICK_TRIGGER' | 'SELECT_RANGE_TRIGGER') {
+      if (await testSubjects.exists(`triggerPicker`)) {
+        const container = await testSubjects.find(`triggerPicker-${trigger}`);
+        const radio = await container.findByCssSelector('input[type=radio]');
+        await radio.click();
+      }
+    }
+
+    async fillInURLTemplate(destinationURLTemplate: string) {
+      await testSubjects.setValue('urlInput', destinationURLTemplate);
+    }
+
     async saveChanges() {
       await testSubjects.click(DRILLDOWN_WIZARD_SUBMIT);
     }
diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx
index 4afd71fd67a6..f3d1eb60bf1c 100644
--- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx
+++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx
@@ -82,6 +82,7 @@ const AppRoot = React.memo(
                 <ResolverWithoutProviders
                   databaseDocumentID=""
                   resolverComponentInstanceID="test"
+                  indices={[]}
                 />
               </Wrapper>
             </Provider>
diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts
new file mode 100644
index 000000000000..7fbba4e04798
--- /dev/null
+++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import expect from '@kbn/expect';
+import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants';
+import { ResolverEntityIndex } from '../../../../plugins/security_solution/common/endpoint/types';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService }: FtrProviderContext) {
+  const supertest = getService('supertest');
+  const esArchiver = getService('esArchiver');
+
+  describe('Resolver tests for the entity route', () => {
+    before(async () => {
+      await esArchiver.load('endpoint/resolver/signals');
+    });
+
+    after(async () => {
+      await esArchiver.unload('endpoint/resolver/signals');
+    });
+
+    it('returns an event even if it does not have a mapping for entity_id', async () => {
+      // this id is from the es archive
+      const _id = 'fa7eb1546f44fd47d8868be8d74e0082e19f22df493c67a7725457978eb648ab';
+      const { body }: { body: ResolverEntityIndex } = await supertest.get(
+        `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default`
+      );
+      expect(body).eql([
+        {
+          // this value is from the es archive
+          entity_id:
+            'MTIwNWY1NWQtODRkYS00MzkxLWIyNWQtYTNkNGJmNDBmY2E1LTc1NTItMTMyNDM1NDY1MTQuNjI0MjgxMDA=',
+        },
+      ]);
+    });
+
+    it('does not return an event when it does not have the entity_id field in the document', async () => {
+      // this id is from the es archive
+      const _id = 'no-entity-id-field';
+      const { body }: { body: ResolverEntityIndex } = await supertest.get(
+        `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default`
+      );
+      expect(body).to.be.empty();
+    });
+
+    it('does not return an event when it does not have the process field in the document', async () => {
+      // this id is from the es archive
+      const _id = 'no-process-field';
+      const { body }: { body: ResolverEntityIndex } = await supertest.get(
+        `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default`
+      );
+      expect(body).to.be.empty();
+    });
+  });
+}
diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts
index fc603af3619a..ecfc1ef5bb7f 100644
--- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts
+++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts
@@ -10,6 +10,7 @@ export default function (providerContext: FtrProviderContext) {
 
   describe('Resolver tests', () => {
     loadTestFile(require.resolve('./entity_id'));
+    loadTestFile(require.resolve('./entity'));
     loadTestFile(require.resolve('./children'));
     loadTestFile(require.resolve('./tree'));
     loadTestFile(require.resolve('./alerts'));
diff --git a/yarn.lock b/yarn.lock
index aef1d5c9ebbe..95066c9fa8cd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4268,10 +4268,10 @@
   dependencies:
     "@types/node" "*"
 
-"@types/node-forge@^0.9.0":
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.0.tgz#e9f678ec09283f9f35cb8de6c01f86be9278ac08"
-  integrity sha512-J00+BIHJOfagO1Qs67Jp5CZO3VkFxY8YKMt44oBhXr+3ZYNnl8wv/vtcJyPjuH0QZ+q7+5nnc6o/YH91ZJy2pQ==
+"@types/node-forge@^0.9.5":
+  version "0.9.5"
+  resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.5.tgz#648231d79da197216290429020698d4e767365a0"
+  integrity sha512-rrN3xfA/oZIzwOnO3d2wRQz7UdeVkmMMPjWUCfpPTPuKFVb3D6G10LuiVHYYmvrivBBLMx4m0P/FICoDbNZUMA==
   dependencies:
     "@types/node" "*"
 
@@ -5590,7 +5590,7 @@ ajv@^4.7.0:
     co "^4.6.0"
     json-stable-stringify "^1.0.1"
 
-ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.9.1:
+ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.5.5, ajv@^6.9.1:
   version "6.12.4"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234"
   integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==
@@ -20631,15 +20631,10 @@ node-forge@0.9.0:
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
   integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
 
-node-forge@^0.7.6:
-  version "0.7.6"
-  resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
-  integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
-
-node-forge@^0.9.1:
-  version "0.9.1"
-  resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
-  integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
+node-forge@^0.10.0, node-forge@^0.7.6:
+  version "0.10.0"
+  resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
+  integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
 
 node-gyp@^3.8.0:
   version "3.8.0"