diff --git a/packages/kbn-discover-utils/src/utils/build_data_record.test.ts b/packages/kbn-discover-utils/src/utils/build_data_record.test.ts
index ad486aba543a1..e6046d4f5977f 100644
--- a/packages/kbn-discover-utils/src/utils/build_data_record.test.ts
+++ b/packages/kbn-discover-utils/src/utils/build_data_record.test.ts
@@ -30,5 +30,17 @@ describe('Data table record utils', () => {
         expect(doc).toHaveProperty('isAnchor');
       });
     });
+
+    test('should support processing each record', () => {
+      const result = buildDataTableRecordList(esHitsMock, dataViewMock, {
+        processRecord: (record) => ({ ...record, id: 'custom-id' }),
+      });
+      result.forEach((doc) => {
+        expect(doc).toHaveProperty('id', 'custom-id');
+        expect(doc).toHaveProperty('raw');
+        expect(doc).toHaveProperty('flattened');
+        expect(doc).toHaveProperty('isAnchor');
+      });
+    });
   });
 });
diff --git a/packages/kbn-discover-utils/src/utils/build_data_record.ts b/packages/kbn-discover-utils/src/utils/build_data_record.ts
index 43adf7b9c8b66..9769201e94aa4 100644
--- a/packages/kbn-discover-utils/src/utils/build_data_record.ts
+++ b/packages/kbn-discover-utils/src/utils/build_data_record.ts
@@ -35,9 +35,13 @@ export function buildDataTableRecord(
  * @param docs Array of documents returned from Elasticsearch
  * @param dataView this current data view
  */
-export function buildDataTableRecordList(
+export function buildDataTableRecordList<T extends DataTableRecord = DataTableRecord>(
   docs: EsHitRecord[],
-  dataView?: DataView
+  dataView?: DataView,
+  { processRecord }: { processRecord?: (record: DataTableRecord) => T } = {}
 ): DataTableRecord[] {
-  return docs.map((doc) => buildDataTableRecord(doc, dataView));
+  return docs.map((doc) => {
+    const record = buildDataTableRecord(doc, dataView);
+    return processRecord ? processRecord(record) : record;
+  });
 }
diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts
index e4e2b71de8e74..f75755319a112 100644
--- a/src/plugins/discover/public/__mocks__/services.ts
+++ b/src/plugins/discover/public/__mocks__/services.ts
@@ -41,6 +41,7 @@ import { SearchSourceDependencies } from '@kbn/data-plugin/common';
 import { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
 import { urlTrackerMock } from './url_tracker.mock';
 import { createElement } from 'react';
+import { createContextAwarenessMocks } from '../context_awareness/__mocks__';
 
 export function createDiscoverServicesMock(): DiscoverServices {
   const dataPlugin = dataPluginMock.createStartContract();
@@ -137,6 +138,7 @@ export function createDiscoverServicesMock(): DiscoverServices {
     ...uiSettingsMock,
   };
 
+  const { profilesManagerMock } = createContextAwarenessMocks();
   const theme = themeServiceMock.createSetupContract({ darkMode: false });
 
   corePluginMock.theme = theme;
@@ -236,6 +238,7 @@ export function createDiscoverServicesMock(): DiscoverServices {
     contextLocator: { getRedirectUrl: jest.fn(() => '') },
     singleDocLocator: { getRedirectUrl: jest.fn(() => '') },
     urlTracker: urlTrackerMock,
+    profilesManager: profilesManagerMock,
     setHeaderActionMenu: jest.fn(),
   } as unknown as DiscoverServices;
 }
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx
index ae4b05f495cfa..85c2dd581eecb 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx
@@ -31,6 +31,7 @@ const customisationService = createCustomizationService();
 
 async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) {
   const services = discoverServiceMock;
+
   services.data.query.timefilter.timefilter.getTime = () => {
     return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
   };
@@ -69,6 +70,10 @@ async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) {
 }
 
 describe('Discover documents layout', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
   test('render loading when loading and no documents', async () => {
     const component = await mountComponent(FetchStatus.LOADING, []);
     expect(component.find('.dscDocuments__loading').exists()).toBeTruthy();
@@ -131,4 +136,22 @@ describe('Discover documents layout', () => {
     expect(discoverGridComponent.prop('externalCustomRenderers')).toBeDefined();
     expect(discoverGridComponent.prop('customGridColumnsConfiguration')).toBeDefined();
   });
+
+  describe('context awareness', () => {
+    it('should pass cell renderers from profile', async () => {
+      customisationService.set({
+        id: 'data_table',
+        logsEnabled: true,
+      });
+      await discoverServiceMock.profilesManager.resolveRootProfile({ solutionNavId: 'test' });
+      const component = await mountComponent(FetchStatus.COMPLETE, esHitsMock);
+      const discoverGridComponent = component.find(DiscoverGrid);
+      expect(discoverGridComponent.exists()).toBeTruthy();
+      expect(Object.keys(discoverGridComponent.prop('externalCustomRenderers')!)).toEqual([
+        'content',
+        'resource',
+        'rootProfile',
+      ]);
+    });
+  });
 });
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
index ad46b7f3db658..caba229e9137a 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
@@ -68,6 +68,7 @@ import { onResizeGridColumn } from '../../../../utils/on_resize_grid_column';
 import { useContextualGridCustomisations } from '../../hooks/grid_customisations';
 import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
 import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups';
+import { useProfileAccessor } from '../../../../context_awareness';
 
 const containerStyles = css`
   position: relative;
@@ -263,6 +264,12 @@ function DiscoverDocumentsComponent({
     useContextualGridCustomisations() || {};
   const additionalFieldGroups = useAdditionalFieldGroups();
 
+  const getCellRenderersAccessor = useProfileAccessor('getCellRenderers');
+  const cellRenderers = useMemo(() => {
+    const getCellRenderers = getCellRenderersAccessor(() => customCellRenderer ?? {});
+    return getCellRenderers();
+  }, [customCellRenderer, getCellRenderersAccessor]);
+
   const documents = useObservable(stateContainer.dataState.data$.documents$);
 
   const callouts = useMemo(
@@ -373,66 +380,64 @@ function DiscoverDocumentsComponent({
           </>
         )}
         {!isLegacy && (
-          <>
-            <div className="unifiedDataTable">
-              <CellActionsProvider
-                getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}
-              >
-                <DiscoverGridMemoized
-                  ariaLabelledBy="documentsAriaLabel"
-                  columns={currentColumns}
-                  columnsMeta={columnsMeta}
-                  expandedDoc={expandedDoc}
-                  dataView={dataView}
-                  loadingState={
-                    isDataLoading
-                      ? DataLoadingState.loading
-                      : isMoreDataLoading
-                      ? DataLoadingState.loadingMore
-                      : DataLoadingState.loaded
-                  }
-                  rows={rows}
-                  sort={(sort as SortOrder[]) || []}
-                  searchDescription={savedSearch.description}
-                  searchTitle={savedSearch.title}
-                  setExpandedDoc={setExpandedDoc}
-                  showTimeCol={showTimeCol}
-                  settings={grid}
-                  onFilter={onAddFilter as DocViewFilterFn}
-                  onSetColumns={onSetColumns}
-                  onSort={onSort}
-                  onResize={onResizeDataGrid}
-                  useNewFieldsApi={useNewFieldsApi}
-                  configHeaderRowHeight={3}
-                  headerRowHeightState={headerRowHeight}
-                  onUpdateHeaderRowHeight={onUpdateHeaderRowHeight}
-                  rowHeightState={rowHeight}
-                  onUpdateRowHeight={onUpdateRowHeight}
-                  isSortEnabled={true}
-                  isPlainRecord={isEsqlMode}
-                  rowsPerPageState={rowsPerPage ?? getDefaultRowsPerPage(services.uiSettings)}
-                  onUpdateRowsPerPage={onUpdateRowsPerPage}
-                  maxAllowedSampleSize={getMaxAllowedSampleSize(services.uiSettings)}
-                  sampleSizeState={getAllowedSampleSize(sampleSizeState, services.uiSettings)}
-                  onUpdateSampleSize={!isEsqlMode ? onUpdateSampleSize : undefined}
-                  onFieldEdited={onFieldEdited}
-                  configRowHeight={uiSettings.get(ROW_HEIGHT_OPTION)}
-                  showMultiFields={uiSettings.get(SHOW_MULTIFIELDS)}
-                  maxDocFieldsDisplayed={uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)}
-                  renderDocumentView={renderDocumentView}
-                  renderCustomToolbar={renderCustomToolbarWithElements}
-                  services={services}
-                  totalHits={totalHits}
-                  onFetchMoreRecords={onFetchMoreRecords}
-                  componentsTourSteps={TOUR_STEPS}
-                  externalCustomRenderers={customCellRenderer}
-                  customGridColumnsConfiguration={customGridColumnsConfiguration}
-                  customControlColumnsConfiguration={customControlColumnsConfiguration}
-                  additionalFieldGroups={additionalFieldGroups}
-                />
-              </CellActionsProvider>
-            </div>
-          </>
+          <div className="unifiedDataTable">
+            <CellActionsProvider
+              getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}
+            >
+              <DiscoverGridMemoized
+                ariaLabelledBy="documentsAriaLabel"
+                columns={currentColumns}
+                columnsMeta={columnsMeta}
+                expandedDoc={expandedDoc}
+                dataView={dataView}
+                loadingState={
+                  isDataLoading
+                    ? DataLoadingState.loading
+                    : isMoreDataLoading
+                    ? DataLoadingState.loadingMore
+                    : DataLoadingState.loaded
+                }
+                rows={rows}
+                sort={(sort as SortOrder[]) || []}
+                searchDescription={savedSearch.description}
+                searchTitle={savedSearch.title}
+                setExpandedDoc={setExpandedDoc}
+                showTimeCol={showTimeCol}
+                settings={grid}
+                onFilter={onAddFilter as DocViewFilterFn}
+                onSetColumns={onSetColumns}
+                onSort={onSort}
+                onResize={onResizeDataGrid}
+                useNewFieldsApi={useNewFieldsApi}
+                configHeaderRowHeight={3}
+                headerRowHeightState={headerRowHeight}
+                onUpdateHeaderRowHeight={onUpdateHeaderRowHeight}
+                rowHeightState={rowHeight}
+                onUpdateRowHeight={onUpdateRowHeight}
+                isSortEnabled={true}
+                isPlainRecord={isEsqlMode}
+                rowsPerPageState={rowsPerPage ?? getDefaultRowsPerPage(services.uiSettings)}
+                onUpdateRowsPerPage={onUpdateRowsPerPage}
+                maxAllowedSampleSize={getMaxAllowedSampleSize(services.uiSettings)}
+                sampleSizeState={getAllowedSampleSize(sampleSizeState, services.uiSettings)}
+                onUpdateSampleSize={!isEsqlMode ? onUpdateSampleSize : undefined}
+                onFieldEdited={onFieldEdited}
+                configRowHeight={uiSettings.get(ROW_HEIGHT_OPTION)}
+                showMultiFields={uiSettings.get(SHOW_MULTIFIELDS)}
+                maxDocFieldsDisplayed={uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)}
+                renderDocumentView={renderDocumentView}
+                renderCustomToolbar={renderCustomToolbarWithElements}
+                services={services}
+                totalHits={totalHits}
+                onFetchMoreRecords={onFetchMoreRecords}
+                componentsTourSteps={TOUR_STEPS}
+                externalCustomRenderers={cellRenderers}
+                customGridColumnsConfiguration={customGridColumnsConfiguration}
+                customControlColumnsConfiguration={customControlColumnsConfiguration}
+                additionalFieldGroups={additionalFieldGroups}
+              />
+            </CellActionsProvider>
+          </div>
         )}
       </EuiFlexItem>
     </>
diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts
index 410d1d468275d..aed3e6f9a0222 100644
--- a/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts
+++ b/src/plugins/discover/public/application/main/data_fetching/fetch_all.ts
@@ -64,7 +64,7 @@ export function fetchAll(
     savedSearch,
     abortController,
   } = fetchDeps;
-  const { data } = services;
+  const { data, expressions, profilesManager } = services;
   const searchSource = savedSearch.searchSource.createChild();
 
   try {
@@ -100,14 +100,15 @@ export function fetchAll(
 
     // Start fetching all required requests
     const response = isEsqlQuery
-      ? fetchEsql(
+      ? fetchEsql({
           query,
           dataView,
-          data,
-          services.expressions,
+          abortSignal: abortController.signal,
           inspectorAdapters,
-          abortController.signal
-        )
+          data,
+          expressions,
+          profilesManager,
+        })
       : fetchDocuments(searchSource, fetchDeps);
     const fetchType = isEsqlQuery ? 'fetchTextBased' : 'fetchDocuments';
     const startTime = window.performance.now();
diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts
index 7abc2d2744a60..be1fddf64e87f 100644
--- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts
+++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.test.ts
@@ -30,6 +30,10 @@ const getDeps = () =>
   } as unknown as FetchDeps);
 
 describe('test fetchDocuments', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
   test('resolves with returned documents', async () => {
     const hits = [
       { _id: '1', foo: 'bar' },
@@ -38,10 +42,17 @@ describe('test fetchDocuments', () => {
     const documents = hits.map((hit) => buildDataTableRecord(hit, dataViewMock));
     savedSearchMock.searchSource.fetch$ = <T>() =>
       of({ rawResponse: { hits: { hits } } } as IKibanaSearchResponse<SearchResponse<T>>);
+    const resolveDocumentProfileSpy = jest.spyOn(
+      discoverServiceMock.profilesManager,
+      'resolveDocumentProfile'
+    );
     expect(await fetchDocuments(savedSearchMock.searchSource, getDeps())).toEqual({
       interceptedWarnings: [],
       records: documents,
     });
+    expect(resolveDocumentProfileSpy).toHaveBeenCalledTimes(2);
+    expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: documents[0] });
+    expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: documents[1] });
   });
 
   test('rejects on query failure', async () => {
diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts
index 414d4b3a36587..4ffdd211c0e5e 100644
--- a/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts
+++ b/src/plugins/discover/public/application/main/data_fetching/fetch_documents.ts
@@ -67,7 +67,9 @@ export const fetchDocuments = (
     .pipe(
       filter((res) => !isRunningResponse(res)),
       map((res) => {
-        return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView);
+        return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView, {
+          processRecord: (record) => services.profilesManager.resolveDocumentProfile({ record }),
+        });
       })
     );
 
diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts
new file mode 100644
index 0000000000000..6546ae8ffaf2d
--- /dev/null
+++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.test.ts
@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { EsHitRecord } from '@kbn/discover-utils';
+import type { ExecutionContract } from '@kbn/expressions-plugin/common';
+import { RequestAdapter } from '@kbn/inspector-plugin/common';
+import { of } from 'rxjs';
+import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
+import { discoverServiceMock } from '../../../__mocks__/services';
+import { fetchEsql } from './fetch_esql';
+
+describe('fetchEsql', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('resolves with returned records', async () => {
+    const hits = [
+      { _id: '1', foo: 'bar' },
+      { _id: '2', foo: 'baz' },
+    ] as unknown as EsHitRecord[];
+    const records = hits.map((hit, i) => ({
+      id: String(i),
+      raw: hit,
+      flattened: hit,
+    }));
+    const expressionsExecuteSpy = jest.spyOn(discoverServiceMock.expressions, 'execute');
+    expressionsExecuteSpy.mockReturnValueOnce({
+      cancel: jest.fn(),
+      getData: jest.fn(() =>
+        of({
+          result: {
+            columns: ['_id', 'foo'],
+            rows: hits,
+          },
+        })
+      ),
+    } as unknown as ExecutionContract);
+    const resolveDocumentProfileSpy = jest.spyOn(
+      discoverServiceMock.profilesManager,
+      'resolveDocumentProfile'
+    );
+    expect(
+      await fetchEsql({
+        query: { esql: 'from *' },
+        dataView: dataViewWithTimefieldMock,
+        inspectorAdapters: { requests: new RequestAdapter() },
+        data: discoverServiceMock.data,
+        expressions: discoverServiceMock.expressions,
+        profilesManager: discoverServiceMock.profilesManager,
+      })
+    ).toEqual({
+      records,
+      esqlQueryColumns: ['_id', 'foo'],
+      esqlHeaderWarning: undefined,
+    });
+    expect(resolveDocumentProfileSpy).toHaveBeenCalledTimes(2);
+    expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: records[0] });
+    expect(resolveDocumentProfileSpy).toHaveBeenCalledWith({ record: records[1] });
+  });
+});
diff --git a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts
index 3aba795d26920..3f54984ae3d3f 100644
--- a/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts
+++ b/src/plugins/discover/public/application/main/data_fetching/fetch_esql.ts
@@ -8,15 +8,16 @@
 import { pluck } from 'rxjs';
 import { lastValueFrom } from 'rxjs';
 import { i18n } from '@kbn/i18n';
-import { Query, AggregateQuery, Filter } from '@kbn/es-query';
+import type { Query, AggregateQuery, Filter } from '@kbn/es-query';
 import type { Adapters } from '@kbn/inspector-plugin/common';
 import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
 import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
 import type { Datatable } from '@kbn/expressions-plugin/public';
 import type { DataView } from '@kbn/data-views-plugin/common';
 import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common';
-import type { DataTableRecord } from '@kbn/discover-utils/types';
+import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils';
 import type { RecordsFetchResponse } from '../../types';
+import type { ProfilesManager } from '../../../context_awareness';
 
 interface EsqlErrorResponse {
   error: {
@@ -25,16 +26,27 @@ interface EsqlErrorResponse {
   type: 'error';
 }
 
-export function fetchEsql(
-  query: Query | AggregateQuery,
-  dataView: DataView,
-  data: DataPublicPluginStart,
-  expressions: ExpressionsStart,
-  inspectorAdapters: Adapters,
-  abortSignal?: AbortSignal,
-  filters?: Filter[],
-  inputQuery?: Query
-): Promise<RecordsFetchResponse> {
+export function fetchEsql({
+  query,
+  inputQuery,
+  filters,
+  dataView,
+  abortSignal,
+  inspectorAdapters,
+  data,
+  expressions,
+  profilesManager,
+}: {
+  query: Query | AggregateQuery;
+  inputQuery?: Query;
+  filters?: Filter[];
+  dataView: DataView;
+  abortSignal?: AbortSignal;
+  inspectorAdapters: Adapters;
+  data: DataPublicPluginStart;
+  expressions: ExpressionsStart;
+  profilesManager: ProfilesManager;
+}): Promise<RecordsFetchResponse> {
   const timeRange = data.query.timefilter.timefilter.getTime();
   return textBasedQueryStateToAstWithValidation({
     filters,
@@ -69,12 +81,14 @@ export function fetchEsql(
             const rows = table?.rows ?? [];
             esqlQueryColumns = table?.columns ?? undefined;
             esqlHeaderWarning = table.warning ?? undefined;
-            finalData = rows.map((row: Record<string, string>, idx: number) => {
-              return {
+            finalData = rows.map((row, idx) => {
+              const record: DataTableRecord = {
                 id: String(idx),
-                raw: row,
+                raw: row as EsHitRecord,
                 flattened: row,
-              } as unknown as DataTableRecord;
+              };
+
+              return profilesManager.resolveDocumentProfile({ record });
             });
           }
         });
@@ -91,7 +105,7 @@ export function fetchEsql(
         });
       }
       return {
-        records: [] as DataTableRecord[],
+        records: [],
         esqlQueryColumns: [],
         esqlHeaderWarning: undefined,
       };
diff --git a/src/plugins/discover/public/application/main/discover_main_route.test.tsx b/src/plugins/discover/public/application/main/discover_main_route.test.tsx
index 496bb91f92cf1..b49abb2fe6685 100644
--- a/src/plugins/discover/public/application/main/discover_main_route.test.tsx
+++ b/src/plugins/discover/public/application/main/discover_main_route.test.tsx
@@ -40,9 +40,22 @@ jest.mock('./discover_main_app', () => {
   };
 });
 
+let mockRootProfileLoading = false;
+
+jest.mock('../../context_awareness', () => {
+  const originalModule = jest.requireActual('../../context_awareness');
+  return {
+    ...originalModule,
+    useRootProfile: () => ({
+      rootProfileLoading: mockRootProfileLoading,
+    }),
+  };
+});
+
 describe('DiscoverMainRoute', () => {
   beforeEach(() => {
     mockCustomizationService = createCustomizationService();
+    mockRootProfileLoading = false;
   });
 
   test('renders the main app when hasESData=true & hasUserDataView=true ', async () => {
@@ -97,6 +110,20 @@ describe('DiscoverMainRoute', () => {
     });
   });
 
+  test('renders LoadingIndicator while root profile is loading', async () => {
+    mockRootProfileLoading = true;
+    const component = mountComponent(true, true);
+    await waitFor(() => {
+      component.update();
+      expect(component.find(DiscoverMainApp).exists()).toBe(false);
+    });
+    mockRootProfileLoading = false;
+    await waitFor(() => {
+      component.setProps({}).update();
+      expect(component.find(DiscoverMainApp).exists()).toBe(true);
+    });
+  });
+
   test('should pass hideNavMenuItems=true to DiscoverTopNavInline while loading', async () => {
     const component = mountComponent(true, true);
     expect(component.find(DiscoverTopNavInline).prop('hideNavMenuItems')).toBe(true);
diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx
index 560f4cb03535e..f37487b6b93b7 100644
--- a/src/plugins/discover/public/application/main/discover_main_route.tsx
+++ b/src/plugins/discover/public/application/main/discover_main_route.tsx
@@ -40,6 +40,7 @@ import {
 import { DiscoverTopNavInline } from './components/top_nav/discover_topnav_inline';
 import { DiscoverStateContainer, LoadParams } from './state_management/discover_state';
 import { DataSourceType, isDataSourceType } from '../../../common/data_sources';
+import { useRootProfile } from '../../context_awareness';
 
 const DiscoverMainAppMemoized = memo(DiscoverMainApp);
 
@@ -338,11 +339,14 @@ export function DiscoverMainRoute({
     stateContainer,
   ]);
 
+  const { solutionNavId } = customizationContext;
+  const { rootProfileLoading } = useRootProfile({ solutionNavId });
+
   if (error) {
     return <DiscoverError error={error} />;
   }
 
-  if (!customizationService) {
+  if (!customizationService || rootProfileLoading) {
     return loadingIndicator;
   }
 
diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts
index 67586670c01c4..05668e0406f9c 100644
--- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts
+++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.test.ts
@@ -26,6 +26,10 @@ jest.mock('@kbn/ebt-tools', () => ({
 const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction<typeof fetchDocuments>;
 
 describe('test getDataStateContainer', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
   test('return is valid', async () => {
     const stateContainer = getDiscoverStateMock({ isTimeBased: true });
     const dataState = stateContainer.dataState;
@@ -35,6 +39,7 @@ describe('test getDataStateContainer', () => {
     expect(dataState.data$.documents$.getValue().fetchStatus).toBe(FetchStatus.LOADING);
     expect(dataState.data$.totalHits$.getValue().fetchStatus).toBe(FetchStatus.LOADING);
   });
+
   test('refetch$ triggers a search', async () => {
     const stateContainer = getDiscoverStateMock({ isTimeBased: true });
     jest.spyOn(stateContainer.searchSessionManager, 'getNextSearchSessionId');
@@ -46,10 +51,15 @@ describe('test getDataStateContainer', () => {
     discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => {
       return { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' };
     });
-    const dataState = stateContainer.dataState;
 
+    const dataState = stateContainer.dataState;
     const unsubscribe = dataState.subscribe();
+    const resolveDataSourceProfileSpy = jest.spyOn(
+      discoverServiceMock.profilesManager,
+      'resolveDataSourceProfile'
+    );
 
+    expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled();
     expect(dataState.data$.totalHits$.value.result).toBe(undefined);
     expect(dataState.data$.documents$.value.result).toEqual(undefined);
 
@@ -58,6 +68,12 @@ describe('test getDataStateContainer', () => {
       expect(dataState.data$.main$.value.fetchStatus).toBe('complete');
     });
 
+    expect(resolveDataSourceProfileSpy).toHaveBeenCalledTimes(1);
+    expect(resolveDataSourceProfileSpy).toHaveBeenCalledWith({
+      dataSource: stateContainer.appState.get().dataSource,
+      dataView: stateContainer.savedSearchState.getState().searchSource.getField('index'),
+      query: stateContainer.appState.get().query,
+    });
     expect(dataState.data$.totalHits$.value.result).toBe(0);
     expect(dataState.data$.documents$.value.result).toEqual([]);
 
@@ -117,9 +133,13 @@ describe('test getDataStateContainer', () => {
     ).not.toHaveBeenCalled();
 
     const dataState = stateContainer.dataState;
-
     const unsubscribe = dataState.subscribe();
+    const resolveDataSourceProfileSpy = jest.spyOn(
+      discoverServiceMock.profilesManager,
+      'resolveDataSourceProfile'
+    );
 
+    expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled();
     expect(dataState.data$.documents$.value.result).toEqual(initialRecords);
 
     let hasLoadingMoreStarted = false;
@@ -131,6 +151,7 @@ describe('test getDataStateContainer', () => {
       }
 
       if (hasLoadingMoreStarted && value.fetchStatus === FetchStatus.COMPLETE) {
+        expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled();
         expect(value.result).toEqual([...initialRecords, ...moreRecords]);
         // it uses the same current search session id
         expect(
diff --git a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts
index 71ad2ed87e79b..aaa1f6c15c0f4 100644
--- a/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts
+++ b/src/plugins/discover/public/application/main/state_management/discover_data_state_container.ts
@@ -152,7 +152,7 @@ export function getDataStateContainer({
   getSavedSearch: () => SavedSearch;
   setDataView: (dataView: DataView) => void;
 }): DiscoverDataStateContainer {
-  const { data, uiSettings, toastNotifications } = services;
+  const { data, uiSettings, toastNotifications, profilesManager } = services;
   const { timefilter } = data.query.timefilter;
   const inspectorAdapters = { requests: new RequestAdapter() };
 
@@ -249,6 +249,12 @@ export function getDataStateContainer({
             return;
           }
 
+          await profilesManager.resolveDataSourceProfile({
+            dataSource: getAppState().dataSource,
+            dataView: getSavedSearch().searchSource.getField('index'),
+            query: getAppState().query,
+          });
+
           abortController = new AbortController();
           const prevAutoRefreshDone = autoRefreshDone;
 
diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts
index e3524dcdf115c..519d6a36fb528 100644
--- a/src/plugins/discover/public/build_services.ts
+++ b/src/plugins/discover/public/build_services.ts
@@ -7,8 +7,7 @@
  */
 
 import { History } from 'history';
-
-import {
+import type {
   Capabilities,
   ChromeStart,
   CoreStart,
@@ -24,28 +23,26 @@ import {
   AppMountParameters,
   ScopedHistory,
 } from '@kbn/core/public';
-import {
+import type {
   FilterManager,
   TimefilterContract,
   DataViewsContract,
   DataPublicPluginStart,
 } from '@kbn/data-plugin/public';
 import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
-import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public';
-import { SharePluginStart } from '@kbn/share-plugin/public';
-import { ChartsPluginStart } from '@kbn/charts-plugin/public';
-import { UiCounterMetricType } from '@kbn/analytics';
+import type { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public';
+import type { SharePluginStart } from '@kbn/share-plugin/public';
+import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
+import type { UiCounterMetricType } from '@kbn/analytics';
 import { Storage } from '@kbn/kibana-utils-plugin/public';
-
-import { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
-import { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
-import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
-import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
-import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
-import { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
-
+import type { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
+import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
+import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
+import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
+import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
+import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
 import type { SpacesApi } from '@kbn/spaces-plugin/public';
-import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
+import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
 import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
 import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
 import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
@@ -59,10 +56,11 @@ import { memoize, noop } from 'lodash';
 import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';
 import type { AiopsPluginStart } from '@kbn/aiops-plugin/public';
 import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public';
-import { DiscoverStartPlugins } from './plugin';
-import { DiscoverContextAppLocator } from './application/context/services/locator';
-import { DiscoverSingleDocLocator } from './application/doc/locator';
-import { DiscoverAppLocator } from '../common';
+import type { DiscoverStartPlugins } from './plugin';
+import type { DiscoverContextAppLocator } from './application/context/services/locator';
+import type { DiscoverSingleDocLocator } from './application/doc/locator';
+import type { DiscoverAppLocator } from '../common';
+import type { ProfilesManager } from './context_awareness';
 
 /**
  * Location state of internal Discover history instance
@@ -129,6 +127,7 @@ export interface DiscoverServices {
   contentClient: ContentClient;
   noDataPage?: NoDataPagePluginStart;
   observabilityAIAssistant?: ObservabilityAIAssistantPublicStart;
+  profilesManager: ProfilesManager;
 }
 
 export const buildServices = memoize(
@@ -142,6 +141,7 @@ export const buildServices = memoize(
     history,
     scopedHistory,
     urlTracker,
+    profilesManager,
     setHeaderActionMenu = noop,
   }: {
     core: CoreStart;
@@ -153,6 +153,7 @@ export const buildServices = memoize(
     history: History<HistoryLocationState>;
     scopedHistory?: ScopedHistory;
     urlTracker: UrlTracker;
+    profilesManager: ProfilesManager;
     setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu'];
   }): DiscoverServices => {
     const { usageCollection } = plugins;
@@ -212,6 +213,7 @@ export const buildServices = memoize(
       contentClient: plugins.contentManagement.client,
       noDataPage: plugins.noDataPage,
       observabilityAIAssistant: plugins.observabilityAIAssistant,
+      profilesManager,
     };
   }
 );
diff --git a/src/plugins/discover/public/components/discover_container/discover_container.tsx b/src/plugins/discover/public/components/discover_container/discover_container.tsx
index 4f768554e1e54..c253760fff2ac 100644
--- a/src/plugins/discover/public/components/discover_container/discover_container.tsx
+++ b/src/plugins/discover/public/components/discover_container/discover_container.tsx
@@ -44,6 +44,7 @@ const discoverContainerWrapperCss = css`
 `;
 
 const customizationContext: DiscoverCustomizationContext = {
+  solutionNavId: null,
   displayMode: 'embedded',
   inlineTopNav: {
     enabled: false,
diff --git a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx
index 3907bc4999232..cb02e3b736663 100644
--- a/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx
+++ b/src/plugins/discover/public/components/discover_grid_flyout/discover_grid_flyout.test.tsx
@@ -25,6 +25,7 @@ import { ReactWrapper } from 'enzyme';
 import { setUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/plugin';
 import { mockUnifiedDocViewerServices } from '@kbn/unified-doc-viewer-plugin/public/__mocks__';
 import { FlyoutCustomization, useDiscoverCustomization } from '../../customizations';
+import { discoverServiceMock } from '../../__mocks__/services';
 
 const mockFlyoutCustomization: FlyoutCustomization = {
   id: 'flyout',
@@ -76,6 +77,7 @@ describe('Discover flyout', function () {
   }) => {
     const onClose = jest.fn();
     const services = {
+      ...discoverServiceMock,
       filterManager: createFilterManagerMock(),
       addBasePath: (path: string) => `/base${path}`,
       history: () => ({ location: {} }),
diff --git a/src/plugins/discover/public/context_awareness/__mocks__/index.ts b/src/plugins/discover/public/context_awareness/__mocks__/index.ts
new file mode 100644
index 0000000000000..0f8beed5d955f
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/__mocks__/index.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { getDataTableRecords } from '../../__fixtures__/real_hits';
+import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
+import {
+  DataSourceCategory,
+  DataSourceProfileProvider,
+  DataSourceProfileService,
+  DocumentProfileProvider,
+  DocumentProfileService,
+  DocumentType,
+  RootProfileProvider,
+  RootProfileService,
+  SolutionType,
+} from '../profiles';
+import { ProfilesManager } from '../profiles_manager';
+
+export const createContextAwarenessMocks = () => {
+  const rootProfileProviderMock: RootProfileProvider = {
+    profileId: 'root-profile',
+    profile: {
+      getCellRenderers: jest.fn((prev) => () => ({
+        ...prev(),
+        rootProfile: () => 'root-profile',
+      })),
+    },
+    resolve: jest.fn(() => ({
+      isMatch: true,
+      context: {
+        solutionType: SolutionType.Observability,
+      },
+    })),
+  };
+
+  const dataSourceProfileProviderMock: DataSourceProfileProvider = {
+    profileId: 'data-source-profile',
+    profile: {
+      getCellRenderers: jest.fn((prev) => () => ({
+        ...prev(),
+        rootProfile: () => 'data-source-profile',
+      })),
+    },
+    resolve: jest.fn(() => ({
+      isMatch: true,
+      context: {
+        category: DataSourceCategory.Logs,
+      },
+    })),
+  };
+
+  const documentProfileProviderMock: DocumentProfileProvider = {
+    profileId: 'document-profile',
+    profile: {
+      getCellRenderers: jest.fn((prev) => () => ({
+        ...prev(),
+        rootProfile: () => 'document-profile',
+      })),
+    } as DocumentProfileProvider['profile'],
+    resolve: jest.fn(() => ({
+      isMatch: true,
+      context: {
+        type: DocumentType.Log,
+      },
+    })),
+  };
+
+  const records = getDataTableRecords(dataViewWithTimefieldMock);
+  const contextRecordMock = records[0];
+  const contextRecordMock2 = records[1];
+
+  const rootProfileServiceMock = new RootProfileService();
+  rootProfileServiceMock.registerProvider(rootProfileProviderMock);
+
+  const dataSourceProfileServiceMock = new DataSourceProfileService();
+  dataSourceProfileServiceMock.registerProvider(dataSourceProfileProviderMock);
+
+  const documentProfileServiceMock = new DocumentProfileService();
+  documentProfileServiceMock.registerProvider(documentProfileProviderMock);
+
+  const profilesManagerMock = new ProfilesManager(
+    rootProfileServiceMock,
+    dataSourceProfileServiceMock,
+    documentProfileServiceMock
+  );
+
+  return {
+    rootProfileProviderMock,
+    dataSourceProfileProviderMock,
+    documentProfileProviderMock,
+    contextRecordMock,
+    contextRecordMock2,
+    profilesManagerMock,
+  };
+};
diff --git a/src/plugins/discover/public/context_awareness/composable_profile.test.ts b/src/plugins/discover/public/context_awareness/composable_profile.test.ts
new file mode 100644
index 0000000000000..251da37fa0126
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/composable_profile.test.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { ComposableProfile, getMergedAccessor } from './composable_profile';
+import { Profile } from './types';
+
+describe('getMergedAccessor', () => {
+  it('should return the base implementation if no profiles are provided', () => {
+    const baseImpl: Profile['getCellRenderers'] = jest.fn(() => ({ base: jest.fn() }));
+    const mergedAccessor = getMergedAccessor([], 'getCellRenderers', baseImpl);
+    const result = mergedAccessor();
+    expect(baseImpl).toHaveBeenCalled();
+    expect(result).toEqual({ base: expect.any(Function) });
+  });
+
+  it('should merge the accessors in the correct order', () => {
+    const baseImpl: Profile['getCellRenderers'] = jest.fn(() => ({ base: jest.fn() }));
+    const profile1: ComposableProfile = {
+      getCellRenderers: jest.fn((prev) => () => ({
+        ...prev(),
+        profile1: jest.fn(),
+      })),
+    };
+    const profile2: ComposableProfile = {
+      getCellRenderers: jest.fn((prev) => () => ({
+        ...prev(),
+        profile2: jest.fn(),
+      })),
+    };
+    const mergedAccessor = getMergedAccessor([profile1, profile2], 'getCellRenderers', baseImpl);
+    const result = mergedAccessor();
+    expect(baseImpl).toHaveBeenCalled();
+    expect(profile1.getCellRenderers).toHaveBeenCalled();
+    expect(profile2.getCellRenderers).toHaveBeenCalled();
+    expect(result).toEqual({
+      base: expect.any(Function),
+      profile1: expect.any(Function),
+      profile2: expect.any(Function),
+    });
+    expect(Object.keys(result)).toEqual(['base', 'profile1', 'profile2']);
+  });
+
+  it('should allow overwriting previous accessors', () => {
+    const baseImpl: Profile['getCellRenderers'] = jest.fn(() => ({ base: jest.fn() }));
+    const profile1: ComposableProfile = {
+      getCellRenderers: jest.fn(() => () => ({ profile1: jest.fn() })),
+    };
+    const profile2: ComposableProfile = {
+      getCellRenderers: jest.fn((prev) => () => ({
+        ...prev(),
+        profile2: jest.fn(),
+      })),
+    };
+    const mergedAccessor = getMergedAccessor([profile1, profile2], 'getCellRenderers', baseImpl);
+    const result = mergedAccessor();
+    expect(baseImpl).not.toHaveBeenCalled();
+    expect(profile1.getCellRenderers).toHaveBeenCalled();
+    expect(profile2.getCellRenderers).toHaveBeenCalled();
+    expect(result).toEqual({ profile1: expect.any(Function), profile2: expect.any(Function) });
+    expect(Object.keys(result)).toEqual(['profile1', 'profile2']);
+  });
+});
diff --git a/src/plugins/discover/public/context_awareness/composable_profile.ts b/src/plugins/discover/public/context_awareness/composable_profile.ts
new file mode 100644
index 0000000000000..c2211dee3f370
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/composable_profile.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Profile } from './types';
+
+export type PartialProfile = Partial<Profile>;
+
+export type ComposableAccessor<T> = (getPrevious: T) => T;
+
+export type ComposableProfile<TProfile extends PartialProfile = Profile> = {
+  [TKey in keyof TProfile]?: ComposableAccessor<TProfile[TKey]>;
+};
+
+export const getMergedAccessor = <TKey extends keyof Profile>(
+  profiles: ComposableProfile[],
+  key: TKey,
+  baseImpl: Profile[TKey]
+) => {
+  return profiles.reduce((nextAccessor, profile) => {
+    const currentAccessor = profile[key];
+    return currentAccessor ? currentAccessor(nextAccessor) : nextAccessor;
+  }, baseImpl);
+};
diff --git a/src/plugins/discover/public/context_awareness/hooks/index.ts b/src/plugins/discover/public/context_awareness/hooks/index.ts
new file mode 100644
index 0000000000000..3235844de4fc5
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/hooks/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { useProfileAccessor } from './use_profile_accessor';
+export { useRootProfile } from './use_root_profile';
diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts
new file mode 100644
index 0000000000000..7f3cd816ae9e8
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.test.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { renderHook } from '@testing-library/react-hooks';
+import { ComposableProfile, getMergedAccessor } from '../composable_profile';
+import { useProfileAccessor } from './use_profile_accessor';
+import { getDataTableRecords } from '../../__fixtures__/real_hits';
+import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
+import { useProfiles } from './use_profiles';
+
+let mockProfiles: ComposableProfile[] = [];
+
+jest.mock('./use_profiles', () => ({
+  useProfiles: jest.fn(() => mockProfiles),
+}));
+
+jest.mock('../composable_profile', () => {
+  const originalModule = jest.requireActual('../composable_profile');
+  return {
+    ...originalModule,
+    getMergedAccessor: jest.fn(originalModule.getMergedAccessor),
+  };
+});
+
+const record = getDataTableRecords(dataViewWithTimefieldMock)[0];
+
+describe('useProfileAccessor', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+    mockProfiles = [
+      { getCellRenderers: (prev) => () => ({ ...prev(), profile1: jest.fn() }) },
+      { getCellRenderers: (prev) => () => ({ ...prev(), profile2: jest.fn() }) },
+    ];
+  });
+
+  it('should return a function that merges accessors', () => {
+    const { result } = renderHook(() => useProfileAccessor('getCellRenderers', { record }));
+    expect(useProfiles).toHaveBeenCalledTimes(1);
+    expect(useProfiles).toHaveBeenCalledWith({ record });
+    const base = () => ({ base: jest.fn() });
+    const accessor = result.current(base);
+    expect(getMergedAccessor).toHaveBeenCalledTimes(1);
+    expect(getMergedAccessor).toHaveBeenCalledWith(mockProfiles, 'getCellRenderers', base);
+    const renderers = accessor();
+    expect(renderers).toEqual({
+      base: expect.any(Function),
+      profile1: expect.any(Function),
+      profile2: expect.any(Function),
+    });
+    expect(Object.keys(renderers)).toEqual(['base', 'profile1', 'profile2']);
+  });
+
+  it('should recalculate the accessor when the key changes', () => {
+    const { rerender, result } = renderHook(({ key }) => useProfileAccessor(key, { record }), {
+      initialProps: { key: 'getCellRenderers' as const },
+    });
+    const prevResult = result.current;
+    rerender({ key: 'getCellRenderers' });
+    expect(result.current).toBe(prevResult);
+    rerender({ key: 'otherKey' as unknown as 'getCellRenderers' });
+    expect(result.current).not.toBe(prevResult);
+  });
+
+  it('should recalculate the accessor when the profiles change', () => {
+    const { rerender, result } = renderHook(() =>
+      useProfileAccessor('getCellRenderers', { record })
+    );
+    const prevResult = result.current;
+    mockProfiles = [{ getCellRenderers: (prev) => () => ({ ...prev(), profile3: jest.fn() }) }];
+    rerender();
+    expect(result.current).not.toBe(prevResult);
+  });
+});
diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts
new file mode 100644
index 0000000000000..58c5a316f86cf
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/hooks/use_profile_accessor.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { useCallback } from 'react';
+import { getMergedAccessor } from '../composable_profile';
+import type { GetProfilesOptions } from '../profiles_manager';
+import { useProfiles } from './use_profiles';
+import type { Profile } from '../types';
+
+export const useProfileAccessor = <TKey extends keyof Profile>(
+  key: TKey,
+  options: GetProfilesOptions = {}
+) => {
+  const profiles = useProfiles(options);
+
+  return useCallback(
+    (baseImpl: Profile[TKey]) => getMergedAccessor(profiles, key, baseImpl),
+    [key, profiles]
+  );
+};
diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx b/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.tsx
new file mode 100644
index 0000000000000..f8613e4fea380
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/hooks/use_profiles.test.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
+import { renderHook } from '@testing-library/react-hooks';
+import React from 'react';
+import { discoverServiceMock } from '../../__mocks__/services';
+import { GetProfilesOptions } from '../profiles_manager';
+import { createContextAwarenessMocks } from '../__mocks__';
+import { useProfiles } from './use_profiles';
+
+const {
+  rootProfileProviderMock,
+  dataSourceProfileProviderMock,
+  documentProfileProviderMock,
+  contextRecordMock,
+  contextRecordMock2,
+  profilesManagerMock,
+} = createContextAwarenessMocks();
+
+profilesManagerMock.resolveRootProfile({});
+profilesManagerMock.resolveDataSourceProfile({});
+
+const record = profilesManagerMock.resolveDocumentProfile({ record: contextRecordMock });
+const record2 = profilesManagerMock.resolveDocumentProfile({ record: contextRecordMock2 });
+
+discoverServiceMock.profilesManager = profilesManagerMock;
+
+const getProfilesSpy = jest.spyOn(discoverServiceMock.profilesManager, 'getProfiles');
+const getProfiles$Spy = jest.spyOn(discoverServiceMock.profilesManager, 'getProfiles$');
+
+const render = () => {
+  return renderHook((props) => useProfiles(props), {
+    initialProps: { record } as GetProfilesOptions,
+    wrapper: ({ children }) => (
+      <KibanaContextProvider services={discoverServiceMock}>{children}</KibanaContextProvider>
+    ),
+  });
+};
+
+describe('useProfiles', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should return profiles', () => {
+    const { result } = render();
+    expect(getProfilesSpy).toHaveBeenCalledTimes(2);
+    expect(getProfiles$Spy).toHaveBeenCalledTimes(1);
+    expect(result.current).toEqual([
+      rootProfileProviderMock.profile,
+      dataSourceProfileProviderMock.profile,
+      documentProfileProviderMock.profile,
+    ]);
+  });
+
+  it('should return the same array reference if profiles do not change', () => {
+    const { result, rerender } = render();
+    expect(getProfilesSpy).toHaveBeenCalledTimes(2);
+    expect(getProfiles$Spy).toHaveBeenCalledTimes(1);
+    const prevResult = result.current;
+    rerender({ record });
+    expect(getProfilesSpy).toHaveBeenCalledTimes(2);
+    expect(getProfiles$Spy).toHaveBeenCalledTimes(1);
+    expect(result.current).toBe(prevResult);
+    rerender({ record: record2 });
+    expect(getProfilesSpy).toHaveBeenCalledTimes(3);
+    expect(getProfiles$Spy).toHaveBeenCalledTimes(2);
+    expect(result.current).toBe(prevResult);
+  });
+
+  it('should return a different array reference if profiles change', () => {
+    const { result, rerender } = render();
+    expect(getProfilesSpy).toHaveBeenCalledTimes(2);
+    expect(getProfiles$Spy).toHaveBeenCalledTimes(1);
+    const prevResult = result.current;
+    rerender({ record });
+    expect(getProfilesSpy).toHaveBeenCalledTimes(2);
+    expect(getProfiles$Spy).toHaveBeenCalledTimes(1);
+    expect(result.current).toBe(prevResult);
+    rerender({ record: undefined });
+    expect(getProfilesSpy).toHaveBeenCalledTimes(3);
+    expect(getProfiles$Spy).toHaveBeenCalledTimes(2);
+    expect(result.current).not.toBe(prevResult);
+  });
+});
diff --git a/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts b/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts
new file mode 100644
index 0000000000000..9bd86e4386150
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/hooks/use_profiles.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { useEffect, useMemo, useState } from 'react';
+import { useDiscoverServices } from '../../hooks/use_discover_services';
+import type { GetProfilesOptions } from '../profiles_manager';
+
+export const useProfiles = ({ record }: GetProfilesOptions = {}) => {
+  const { profilesManager } = useDiscoverServices();
+  const [profiles, setProfiles] = useState(() => profilesManager.getProfiles({ record }));
+  const profiles$ = useMemo(
+    () => profilesManager.getProfiles$({ record }),
+    [profilesManager, record]
+  );
+
+  useEffect(() => {
+    const subscription = profiles$.subscribe((newProfiles) => {
+      setProfiles((currentProfiles) => {
+        return currentProfiles.every((profile, i) => profile === newProfiles[i])
+          ? currentProfiles
+          : newProfiles;
+      });
+    });
+
+    return () => {
+      subscription.unsubscribe();
+    };
+  }, [profiles$]);
+
+  return profiles;
+};
diff --git a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.test.tsx b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.test.tsx
new file mode 100644
index 0000000000000..a41ec7c23cf88
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
+import { renderHook } from '@testing-library/react-hooks';
+import React from 'react';
+import { discoverServiceMock } from '../../__mocks__/services';
+import { useRootProfile } from './use_root_profile';
+
+const render = () => {
+  return renderHook((props) => useRootProfile(props), {
+    initialProps: { solutionNavId: 'solutionNavId' },
+    wrapper: ({ children }) => (
+      <KibanaContextProvider services={discoverServiceMock}>{children}</KibanaContextProvider>
+    ),
+  });
+};
+
+describe('useRootProfile', () => {
+  it('should return rootProfileLoading as true', () => {
+    const { result } = render();
+    expect(result.current.rootProfileLoading).toBe(true);
+  });
+
+  it('should return rootProfileLoading as false', async () => {
+    const { result, waitForNextUpdate } = render();
+    await waitForNextUpdate();
+    expect(result.current.rootProfileLoading).toBe(false);
+  });
+
+  it('should return rootProfileLoading as true when solutionNavId changes', async () => {
+    const { result, rerender, waitForNextUpdate } = render();
+    await waitForNextUpdate();
+    expect(result.current.rootProfileLoading).toBe(false);
+    rerender({ solutionNavId: 'newSolutionNavId' });
+    expect(result.current.rootProfileLoading).toBe(true);
+    await waitForNextUpdate();
+    expect(result.current.rootProfileLoading).toBe(false);
+  });
+});
diff --git a/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts
new file mode 100644
index 0000000000000..ff2d7edbcefb8
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/hooks/use_root_profile.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { useEffect, useState } from 'react';
+import { useDiscoverServices } from '../../hooks/use_discover_services';
+
+export const useRootProfile = ({ solutionNavId }: { solutionNavId: string | null }) => {
+  const { profilesManager } = useDiscoverServices();
+  const [rootProfileLoading, setRootProfileLoading] = useState(true);
+
+  useEffect(() => {
+    let aborted = false;
+
+    setRootProfileLoading(true);
+
+    profilesManager.resolveRootProfile({ solutionNavId }).then(() => {
+      if (!aborted) {
+        setRootProfileLoading(false);
+      }
+    });
+
+    return () => {
+      aborted = true;
+    };
+  }, [profilesManager, solutionNavId]);
+
+  return { rootProfileLoading };
+};
diff --git a/src/plugins/discover/public/context_awareness/index.ts b/src/plugins/discover/public/context_awareness/index.ts
new file mode 100644
index 0000000000000..6106d9d154e49
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './types';
+export * from './profiles';
+export { getMergedAccessor } from './composable_profile';
+export { ProfilesManager } from './profiles_manager';
+export { useProfileAccessor, useRootProfile } from './hooks';
diff --git a/src/plugins/discover/public/context_awareness/profile_service.test.ts b/src/plugins/discover/public/context_awareness/profile_service.test.ts
new file mode 100644
index 0000000000000..e306ee149f52f
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/profile_service.test.ts
@@ -0,0 +1,147 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+/* eslint-disable max-classes-per-file */
+
+import { AsyncProfileService, ContextWithProfileId, ProfileService } from './profile_service';
+import { Profile } from './types';
+
+interface TestParams {
+  myParam: string;
+}
+
+interface TestContext {
+  myContext: string;
+}
+
+const defaultContext: ContextWithProfileId<TestContext> = {
+  profileId: 'test-profile',
+  myContext: 'test',
+};
+
+class TestProfileService extends ProfileService<Profile, TestParams, TestContext> {
+  constructor() {
+    super(defaultContext);
+  }
+}
+
+type TestProfileProvider = Parameters<TestProfileService['registerProvider']>[0];
+
+class TestAsyncProfileService extends AsyncProfileService<Profile, TestParams, TestContext> {
+  constructor() {
+    super(defaultContext);
+  }
+}
+
+type TestAsyncProfileProvider = Parameters<TestAsyncProfileService['registerProvider']>[0];
+
+const provider: TestProfileProvider = {
+  profileId: 'test-profile-1',
+  profile: { getCellRenderers: jest.fn() },
+  resolve: jest.fn(() => ({ isMatch: false })),
+};
+
+const provider2: TestProfileProvider = {
+  profileId: 'test-profile-2',
+  profile: { getCellRenderers: jest.fn() },
+  resolve: jest.fn(({ myParam }) => ({ isMatch: true, context: { myContext: myParam } })),
+};
+
+const provider3: TestProfileProvider = {
+  profileId: 'test-profile-3',
+  profile: { getCellRenderers: jest.fn() },
+  resolve: jest.fn(({ myParam }) => ({ isMatch: true, context: { myContext: myParam } })),
+};
+
+const asyncProvider2: TestAsyncProfileProvider = {
+  profileId: 'test-profile-2',
+  profile: { getCellRenderers: jest.fn() },
+  resolve: jest.fn(async ({ myParam }) => ({ isMatch: true, context: { myContext: myParam } })),
+};
+
+describe('ProfileService', () => {
+  let service: TestProfileService;
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    service = new TestProfileService();
+  });
+
+  it('should expose defaultContext', () => {
+    expect(service.defaultContext).toBe(defaultContext);
+  });
+
+  it('should allow registering providers and getting profiles', () => {
+    service.registerProvider(provider);
+    service.registerProvider(provider2);
+    expect(service.getProfile({ profileId: 'test-profile-1', myContext: 'test' })).toBe(
+      provider.profile
+    );
+    expect(service.getProfile({ profileId: 'test-profile-2', myContext: 'test' })).toBe(
+      provider2.profile
+    );
+  });
+
+  it('should return empty profile if no provider is found', () => {
+    service.registerProvider(provider);
+    expect(service.getProfile({ profileId: 'test-profile-2', myContext: 'test' })).toEqual({});
+  });
+
+  it('should resolve to first matching context', () => {
+    service.registerProvider(provider);
+    service.registerProvider(provider2);
+    service.registerProvider(provider3);
+    expect(service.resolve({ myParam: 'test' })).toEqual({
+      profileId: 'test-profile-2',
+      myContext: 'test',
+    });
+    expect(provider.resolve).toHaveBeenCalledTimes(1);
+    expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' });
+    expect(provider2.resolve).toHaveBeenCalledTimes(1);
+    expect(provider2.resolve).toHaveBeenCalledWith({ myParam: 'test' });
+    expect(provider3.resolve).not.toHaveBeenCalled();
+  });
+
+  it('should resolve to default context if no matching context is found', () => {
+    service.registerProvider(provider);
+    expect(service.resolve({ myParam: 'test' })).toEqual(defaultContext);
+    expect(provider.resolve).toHaveBeenCalledTimes(1);
+    expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' });
+  });
+});
+
+describe('AsyncProfileService', () => {
+  let service: TestAsyncProfileService;
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    service = new TestAsyncProfileService();
+  });
+
+  it('should resolve to first matching context', async () => {
+    service.registerProvider(provider);
+    service.registerProvider(asyncProvider2);
+    service.registerProvider(provider3);
+    await expect(service.resolve({ myParam: 'test' })).resolves.toEqual({
+      profileId: 'test-profile-2',
+      myContext: 'test',
+    });
+    expect(provider.resolve).toHaveBeenCalledTimes(1);
+    expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' });
+    expect(asyncProvider2.resolve).toHaveBeenCalledTimes(1);
+    expect(asyncProvider2.resolve).toHaveBeenCalledWith({ myParam: 'test' });
+    expect(provider3.resolve).not.toHaveBeenCalled();
+  });
+
+  it('should resolve to default context if no matching context is found', async () => {
+    service.registerProvider(provider);
+    await expect(service.resolve({ myParam: 'test' })).resolves.toEqual(defaultContext);
+    expect(provider.resolve).toHaveBeenCalledTimes(1);
+    expect(provider.resolve).toHaveBeenCalledWith({ myParam: 'test' });
+  });
+});
diff --git a/src/plugins/discover/public/context_awareness/profile_service.ts b/src/plugins/discover/public/context_awareness/profile_service.ts
new file mode 100644
index 0000000000000..2b43595761d19
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/profile_service.ts
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+/* eslint-disable max-classes-per-file */
+
+import type { ComposableProfile, PartialProfile } from './composable_profile';
+import type { Profile } from './types';
+
+export type ResolveProfileResult<TContext> =
+  | { isMatch: true; context: TContext }
+  | { isMatch: false };
+
+export type ProfileProviderMode = 'sync' | 'async';
+
+export interface ProfileProvider<
+  TProfile extends PartialProfile,
+  TParams,
+  TContext,
+  TMode extends ProfileProviderMode
+> {
+  profileId: string;
+  profile: ComposableProfile<TProfile>;
+  resolve: (
+    params: TParams
+  ) => TMode extends 'sync'
+    ? ResolveProfileResult<TContext>
+    : ResolveProfileResult<TContext> | Promise<ResolveProfileResult<TContext>>;
+}
+
+export type ContextWithProfileId<TContext> = TContext & { profileId: string };
+
+const EMPTY_PROFILE = {};
+
+abstract class BaseProfileService<
+  TProfile extends PartialProfile,
+  TParams,
+  TContext,
+  TMode extends ProfileProviderMode
+> {
+  protected readonly providers: Array<ProfileProvider<TProfile, TParams, TContext, TMode>> = [];
+
+  protected constructor(public readonly defaultContext: ContextWithProfileId<TContext>) {}
+
+  public registerProvider(provider: ProfileProvider<TProfile, TParams, TContext, TMode>) {
+    this.providers.push(provider);
+  }
+
+  public getProfile(context: ContextWithProfileId<TContext>): ComposableProfile<Profile> {
+    const provider = this.providers.find((current) => current.profileId === context.profileId);
+    return provider?.profile ?? EMPTY_PROFILE;
+  }
+
+  public abstract resolve(
+    params: TParams
+  ): TMode extends 'sync'
+    ? ContextWithProfileId<TContext>
+    : Promise<ContextWithProfileId<TContext>>;
+}
+
+export class ProfileService<
+  TProfile extends PartialProfile,
+  TParams,
+  TContext
+> extends BaseProfileService<TProfile, TParams, TContext, 'sync'> {
+  public resolve(params: TParams) {
+    for (const provider of this.providers) {
+      const result = provider.resolve(params);
+
+      if (result.isMatch) {
+        return {
+          ...result.context,
+          profileId: provider.profileId,
+        };
+      }
+    }
+
+    return this.defaultContext;
+  }
+}
+
+export class AsyncProfileService<
+  TProfile extends PartialProfile,
+  TParams,
+  TContext
+> extends BaseProfileService<TProfile, TParams, TContext, 'async'> {
+  public async resolve(params: TParams) {
+    for (const provider of this.providers) {
+      const result = await provider.resolve(params);
+
+      if (result.isMatch) {
+        return {
+          ...result.context,
+          profileId: provider.profileId,
+        };
+      }
+    }
+
+    return this.defaultContext;
+  }
+}
diff --git a/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts
new file mode 100644
index 0000000000000..f616fef913259
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/profiles/data_source_profile.ts
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { DataView } from '@kbn/data-views-plugin/common';
+import type { AggregateQuery, Query } from '@kbn/es-query';
+import type { DiscoverDataSource } from '../../../common/data_sources';
+import { AsyncProfileService } from '../profile_service';
+import { Profile } from '../types';
+
+export enum DataSourceCategory {
+  Logs = 'logs',
+  Default = 'default',
+}
+
+export interface DataSourceProfileProviderParams {
+  dataSource?: DiscoverDataSource;
+  dataView?: DataView;
+  query?: Query | AggregateQuery;
+}
+
+export interface DataSourceContext {
+  category: DataSourceCategory;
+}
+
+export type DataSourceProfile = Profile;
+
+export class DataSourceProfileService extends AsyncProfileService<
+  DataSourceProfile,
+  DataSourceProfileProviderParams,
+  DataSourceContext
+> {
+  constructor() {
+    super({
+      profileId: 'default-data-source-profile',
+      category: DataSourceCategory.Default,
+    });
+  }
+}
+
+export type DataSourceProfileProvider = Parameters<DataSourceProfileService['registerProvider']>[0];
diff --git a/src/plugins/discover/public/context_awareness/profiles/document_profile.ts b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts
new file mode 100644
index 0000000000000..70b134da452e4
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/profiles/document_profile.ts
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { DataTableRecord } from '@kbn/discover-utils';
+import type { Profile } from '../types';
+import { ProfileService } from '../profile_service';
+
+export enum DocumentType {
+  Log = 'log',
+  Default = 'default',
+}
+
+export interface DocumentProfileProviderParams {
+  record: DataTableRecord;
+}
+
+export interface DocumentContext {
+  type: DocumentType;
+}
+
+export type DocumentProfile = Omit<Profile, 'getCellRenderers'>;
+
+export class DocumentProfileService extends ProfileService<
+  DocumentProfile,
+  DocumentProfileProviderParams,
+  DocumentContext
+> {
+  constructor() {
+    super({
+      profileId: 'default-document-profile',
+      type: DocumentType.Default,
+    });
+  }
+}
+
+export type DocumentProfileProvider = Parameters<DocumentProfileService['registerProvider']>[0];
diff --git a/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx
new file mode 100644
index 0000000000000..3835337b25304
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/profiles/example_profiles.tsx
@@ -0,0 +1,123 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiBadge } from '@elastic/eui';
+import {
+  DataTableRecord,
+  getMessageFieldWithFallbacks,
+  LogDocumentOverview,
+} from '@kbn/discover-utils';
+import { isOfAggregateQueryType } from '@kbn/es-query';
+import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
+import { euiThemeVars } from '@kbn/ui-theme';
+import { capitalize } from 'lodash';
+import React from 'react';
+import { DataSourceType, isDataSourceType } from '../../../common/data_sources';
+import { DataSourceCategory, DataSourceProfileProvider } from './data_source_profile';
+import { DocumentProfileProvider, DocumentType } from './document_profile';
+import { RootProfileProvider, SolutionType } from './root_profile';
+
+export const o11yRootProfileProvider: RootProfileProvider = {
+  profileId: 'o11y-root-profile',
+  profile: {},
+  resolve: (params) => {
+    if (params.solutionNavId === 'oblt') {
+      return {
+        isMatch: true,
+        context: {
+          solutionType: SolutionType.Observability,
+        },
+      };
+    }
+
+    return { isMatch: false };
+  },
+};
+
+export const logsDataSourceProfileProvider: DataSourceProfileProvider = {
+  profileId: 'logs-data-source-profile',
+  profile: {
+    getCellRenderers: (prev) => () => ({
+      ...prev(),
+      '@timestamp': (props) => {
+        const timestamp = getFieldValue(props.row, '@timestamp');
+        return (
+          <EuiBadge color="hollow" title={timestamp}>
+            {timestamp}
+          </EuiBadge>
+        );
+      },
+      'log.level': (props) => {
+        const level = getFieldValue(props.row, 'log.level');
+        if (!level) {
+          return <span css={{ color: euiThemeVars.euiTextSubduedColor }}>(None)</span>;
+        }
+        const levelMap: Record<string, string> = {
+          info: 'primary',
+          debug: 'default',
+          error: 'danger',
+        };
+        return (
+          <EuiBadge color={levelMap[level]} title={level}>
+            {capitalize(level)}
+          </EuiBadge>
+        );
+      },
+      message: (props) => {
+        const { value } = getMessageFieldWithFallbacks(
+          props.row.flattened as unknown as LogDocumentOverview
+        );
+        return value || <span css={{ color: euiThemeVars.euiTextSubduedColor }}>(None)</span>;
+      },
+    }),
+  },
+  resolve: (params) => {
+    let indices: string[] = [];
+
+    if (isDataSourceType(params.dataSource, DataSourceType.Esql)) {
+      if (!isOfAggregateQueryType(params.query)) {
+        return { isMatch: false };
+      }
+
+      indices = getIndexPatternFromESQLQuery(params.query.esql).split(',');
+    } else if (isDataSourceType(params.dataSource, DataSourceType.DataView) && params.dataView) {
+      indices = params.dataView.getIndexPattern().split(',');
+    }
+
+    if (indices.every((index) => index.startsWith('logs-'))) {
+      return {
+        isMatch: true,
+        context: { category: DataSourceCategory.Logs },
+      };
+    }
+
+    return { isMatch: false };
+  },
+};
+
+export const logDocumentProfileProvider: DocumentProfileProvider = {
+  profileId: 'log-document-profile',
+  profile: {},
+  resolve: (params) => {
+    if (getFieldValue(params.record, 'data_stream.type') === 'logs') {
+      return {
+        isMatch: true,
+        context: {
+          type: DocumentType.Log,
+        },
+      };
+    }
+
+    return { isMatch: false };
+  },
+};
+
+const getFieldValue = (record: DataTableRecord, field: string) => {
+  const value = record.flattened[field];
+  return Array.isArray(value) ? value[0] : value;
+};
diff --git a/src/plugins/discover/public/context_awareness/profiles/index.ts b/src/plugins/discover/public/context_awareness/profiles/index.ts
new file mode 100644
index 0000000000000..f661276b4a04c
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/profiles/index.ts
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './root_profile';
+export * from './data_source_profile';
+export * from './document_profile';
diff --git a/src/plugins/discover/public/context_awareness/profiles/root_profile.ts b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts
new file mode 100644
index 0000000000000..42497fe680c5c
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/profiles/root_profile.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Profile } from '../types';
+import { AsyncProfileService } from '../profile_service';
+
+export enum SolutionType {
+  Observability = 'oblt',
+  Security = 'security',
+  Search = 'search',
+  Default = 'default',
+}
+
+export interface RootProfileProviderParams {
+  solutionNavId?: string | null;
+}
+
+export interface RootContext {
+  solutionType: SolutionType;
+}
+
+export type RootProfile = Profile;
+
+export class RootProfileService extends AsyncProfileService<
+  RootProfile,
+  RootProfileProviderParams,
+  RootContext
+> {
+  constructor() {
+    super({
+      profileId: 'default-root-profile',
+      solutionType: SolutionType.Default,
+    });
+  }
+}
+
+export type RootProfileProvider = Parameters<RootProfileService['registerProvider']>[0];
diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.test.ts b/src/plugins/discover/public/context_awareness/profiles_manager.test.ts
new file mode 100644
index 0000000000000..153ef979aabba
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/profiles_manager.test.ts
@@ -0,0 +1,263 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { firstValueFrom, Subject } from 'rxjs';
+import { createEsqlDataSource } from '../../common/data_sources';
+import { addLog } from '../utils/add_log';
+import { createContextAwarenessMocks } from './__mocks__';
+
+jest.mock('../utils/add_log');
+
+let mocks = createContextAwarenessMocks();
+
+describe('ProfilesManager', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+    mocks = createContextAwarenessMocks();
+  });
+
+  it('should return default profiles', () => {
+    const profiles = mocks.profilesManagerMock.getProfiles();
+    expect(profiles).toEqual([{}, {}, {}]);
+  });
+
+  it('should resolve root profile', async () => {
+    await mocks.profilesManagerMock.resolveRootProfile({});
+    const profiles = mocks.profilesManagerMock.getProfiles();
+    expect(profiles).toEqual([mocks.rootProfileProviderMock.profile, {}, {}]);
+  });
+
+  it('should resolve data source profile', async () => {
+    await mocks.profilesManagerMock.resolveDataSourceProfile({});
+    const profiles = mocks.profilesManagerMock.getProfiles();
+    expect(profiles).toEqual([{}, mocks.dataSourceProfileProviderMock.profile, {}]);
+  });
+
+  it('should resolve document profile', async () => {
+    const record = mocks.profilesManagerMock.resolveDocumentProfile({
+      record: mocks.contextRecordMock,
+    });
+    const profiles = mocks.profilesManagerMock.getProfiles({ record });
+    expect(profiles).toEqual([{}, {}, mocks.documentProfileProviderMock.profile]);
+  });
+
+  it('should resolve multiple profiles', async () => {
+    await mocks.profilesManagerMock.resolveRootProfile({});
+    await mocks.profilesManagerMock.resolveDataSourceProfile({});
+    const record = mocks.profilesManagerMock.resolveDocumentProfile({
+      record: mocks.contextRecordMock,
+    });
+    const profiles = mocks.profilesManagerMock.getProfiles({ record });
+    expect(profiles).toEqual([
+      mocks.rootProfileProviderMock.profile,
+      mocks.dataSourceProfileProviderMock.profile,
+      mocks.documentProfileProviderMock.profile,
+    ]);
+  });
+
+  it('should expose profiles as an observable', async () => {
+    const getProfilesSpy = jest.spyOn(mocks.profilesManagerMock, 'getProfiles');
+    const record = mocks.profilesManagerMock.resolveDocumentProfile({
+      record: mocks.contextRecordMock,
+    });
+    const profiles$ = mocks.profilesManagerMock.getProfiles$({ record });
+    const next = jest.fn();
+    profiles$.subscribe(next);
+    expect(getProfilesSpy).toHaveBeenCalledTimes(1);
+    expect(next).toHaveBeenCalledWith([{}, {}, mocks.documentProfileProviderMock.profile]);
+    await mocks.profilesManagerMock.resolveRootProfile({});
+    expect(getProfilesSpy).toHaveBeenCalledTimes(2);
+    expect(next).toHaveBeenCalledWith([
+      mocks.rootProfileProviderMock.profile,
+      {},
+      mocks.documentProfileProviderMock.profile,
+    ]);
+    await mocks.profilesManagerMock.resolveDataSourceProfile({});
+    expect(getProfilesSpy).toHaveBeenCalledTimes(3);
+    expect(next).toHaveBeenCalledWith([
+      mocks.rootProfileProviderMock.profile,
+      mocks.dataSourceProfileProviderMock.profile,
+      mocks.documentProfileProviderMock.profile,
+    ]);
+  });
+
+  it("should not resolve root profile again if params haven't changed", async () => {
+    await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' });
+    await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' });
+    expect(mocks.rootProfileProviderMock.resolve).toHaveBeenCalledTimes(1);
+  });
+
+  it('should resolve root profile again if params have changed', async () => {
+    await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' });
+    await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'newSolutionNavId' });
+    expect(mocks.rootProfileProviderMock.resolve).toHaveBeenCalledTimes(2);
+  });
+
+  it('should not resolve data source profile again if params have not changed', async () => {
+    await mocks.profilesManagerMock.resolveDataSourceProfile({
+      dataSource: createEsqlDataSource(),
+      query: { esql: 'from *' },
+    });
+    expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(1);
+    await mocks.profilesManagerMock.resolveDataSourceProfile({
+      dataSource: createEsqlDataSource(),
+      query: { esql: 'from *' },
+    });
+    expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(1);
+  });
+
+  it('should resolve data source profile again if params have changed', async () => {
+    await mocks.profilesManagerMock.resolveDataSourceProfile({
+      dataSource: createEsqlDataSource(),
+      query: { esql: 'from *' },
+    });
+    expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(1);
+    await mocks.profilesManagerMock.resolveDataSourceProfile({
+      dataSource: createEsqlDataSource(),
+      query: { esql: 'from logs-*' },
+    });
+    expect(mocks.dataSourceProfileProviderMock.resolve).toHaveBeenCalledTimes(2);
+  });
+
+  it('should log an error and fall back to the default profile if root profile resolution fails', async () => {
+    await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'solutionNavId' });
+    let profiles = mocks.profilesManagerMock.getProfiles();
+    expect(profiles).toEqual([mocks.rootProfileProviderMock.profile, {}, {}]);
+    const resolveSpy = jest.spyOn(mocks.rootProfileProviderMock, 'resolve');
+    resolveSpy.mockRejectedValue(new Error('Failed to resolve'));
+    await mocks.profilesManagerMock.resolveRootProfile({ solutionNavId: 'newSolutionNavId' });
+    expect(addLog).toHaveBeenCalledWith(
+      '[ProfilesManager] root context resolution failed with params: {\n  "solutionNavId": "newSolutionNavId"\n}',
+      new Error('Failed to resolve')
+    );
+    profiles = mocks.profilesManagerMock.getProfiles();
+    expect(profiles).toEqual([{}, {}, {}]);
+  });
+
+  it('should log an error and fall back to the default profile if data source profile resolution fails', async () => {
+    await mocks.profilesManagerMock.resolveDataSourceProfile({
+      dataSource: createEsqlDataSource(),
+      query: { esql: 'from *' },
+    });
+    let profiles = mocks.profilesManagerMock.getProfiles();
+    expect(profiles).toEqual([{}, mocks.dataSourceProfileProviderMock.profile, {}]);
+    const resolveSpy = jest.spyOn(mocks.dataSourceProfileProviderMock, 'resolve');
+    resolveSpy.mockRejectedValue(new Error('Failed to resolve'));
+    await mocks.profilesManagerMock.resolveDataSourceProfile({
+      dataSource: createEsqlDataSource(),
+      query: { esql: 'from logs-*' },
+    });
+    expect(addLog).toHaveBeenCalledWith(
+      '[ProfilesManager] data source context resolution failed with params: {\n  "esqlQuery": "from logs-*"\n}',
+      new Error('Failed to resolve')
+    );
+    profiles = mocks.profilesManagerMock.getProfiles();
+    expect(profiles).toEqual([{}, {}, {}]);
+  });
+
+  it('should log an error and fall back to the default profile if document profile resolution fails', () => {
+    const record = mocks.profilesManagerMock.resolveDocumentProfile({
+      record: mocks.contextRecordMock,
+    });
+    let profiles = mocks.profilesManagerMock.getProfiles({ record });
+    expect(profiles).toEqual([{}, {}, mocks.documentProfileProviderMock.profile]);
+    const resolveSpy = jest.spyOn(mocks.documentProfileProviderMock, 'resolve');
+    resolveSpy.mockImplementation(() => {
+      throw new Error('Failed to resolve');
+    });
+    const record2 = mocks.profilesManagerMock.resolveDocumentProfile({
+      record: mocks.contextRecordMock2,
+    });
+    profiles = mocks.profilesManagerMock.getProfiles({ record: record2 });
+    expect(addLog).toHaveBeenCalledWith(
+      '[ProfilesManager] document context resolution failed with params: {\n  "recordId": "logstash-2014.09.09::388::"\n}',
+      new Error('Failed to resolve')
+    );
+    expect(profiles).toEqual([{}, {}, {}]);
+  });
+
+  it('should cancel existing root profile resolution when another is triggered', async () => {
+    const context = await mocks.rootProfileProviderMock.resolve({ solutionNavId: 'solutionNavId' });
+    const newContext = await mocks.rootProfileProviderMock.resolve({
+      solutionNavId: 'newSolutionNavId',
+    });
+    const resolveSpy = jest.spyOn(mocks.rootProfileProviderMock, 'resolve');
+    resolveSpy.mockClear();
+    const resolvedDeferredResult$ = new Subject();
+    const deferredResult = firstValueFrom(resolvedDeferredResult$).then(() => context);
+    resolveSpy.mockResolvedValueOnce(deferredResult);
+    const promise1 = mocks.profilesManagerMock.resolveRootProfile({
+      solutionNavId: 'solutionNavId',
+    });
+    expect(resolveSpy).toHaveReturnedTimes(1);
+    expect(resolveSpy).toHaveLastReturnedWith(deferredResult);
+    expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]);
+    const resolvedDeferredResult2$ = new Subject();
+    const deferredResult2 = firstValueFrom(resolvedDeferredResult2$).then(() => newContext);
+    resolveSpy.mockResolvedValueOnce(deferredResult2);
+    const promise2 = mocks.profilesManagerMock.resolveRootProfile({
+      solutionNavId: 'newSolutionNavId',
+    });
+    expect(resolveSpy).toHaveReturnedTimes(2);
+    expect(resolveSpy).toHaveLastReturnedWith(deferredResult2);
+    expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]);
+    resolvedDeferredResult$.next(undefined);
+    await promise1;
+    expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]);
+    resolvedDeferredResult2$.next(undefined);
+    await promise2;
+    expect(mocks.profilesManagerMock.getProfiles()).toEqual([
+      mocks.rootProfileProviderMock.profile,
+      {},
+      {},
+    ]);
+  });
+
+  it('should cancel existing data source profile resolution when another is triggered', async () => {
+    const context = await mocks.dataSourceProfileProviderMock.resolve({
+      dataSource: createEsqlDataSource(),
+      query: { esql: 'from *' },
+    });
+    const newContext = await mocks.dataSourceProfileProviderMock.resolve({
+      dataSource: createEsqlDataSource(),
+      query: { esql: 'from logs-*' },
+    });
+    const resolveSpy = jest.spyOn(mocks.dataSourceProfileProviderMock, 'resolve');
+    resolveSpy.mockClear();
+    const resolvedDeferredResult$ = new Subject();
+    const deferredResult = firstValueFrom(resolvedDeferredResult$).then(() => context);
+    resolveSpy.mockResolvedValueOnce(deferredResult);
+    const promise1 = mocks.profilesManagerMock.resolveDataSourceProfile({
+      dataSource: createEsqlDataSource(),
+      query: { esql: 'from *' },
+    });
+    expect(resolveSpy).toHaveReturnedTimes(1);
+    expect(resolveSpy).toHaveLastReturnedWith(deferredResult);
+    expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]);
+    const resolvedDeferredResult2$ = new Subject();
+    const deferredResult2 = firstValueFrom(resolvedDeferredResult2$).then(() => newContext);
+    resolveSpy.mockResolvedValueOnce(deferredResult2);
+    const promise2 = mocks.profilesManagerMock.resolveDataSourceProfile({
+      dataSource: createEsqlDataSource(),
+      query: { esql: 'from logs-*' },
+    });
+    expect(resolveSpy).toHaveReturnedTimes(2);
+    expect(resolveSpy).toHaveLastReturnedWith(deferredResult2);
+    expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]);
+    resolvedDeferredResult$.next(undefined);
+    await promise1;
+    expect(mocks.profilesManagerMock.getProfiles()).toEqual([{}, {}, {}]);
+    resolvedDeferredResult2$.next(undefined);
+    await promise2;
+    expect(mocks.profilesManagerMock.getProfiles()).toEqual([
+      {},
+      mocks.dataSourceProfileProviderMock.profile,
+      {},
+    ]);
+  });
+});
diff --git a/src/plugins/discover/public/context_awareness/profiles_manager.ts b/src/plugins/discover/public/context_awareness/profiles_manager.ts
new file mode 100644
index 0000000000000..316419d2a7d3f
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/profiles_manager.ts
@@ -0,0 +1,206 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { DataTableRecord } from '@kbn/discover-utils';
+import { isOfAggregateQueryType } from '@kbn/es-query';
+import { isEqual } from 'lodash';
+import { BehaviorSubject, combineLatest, map } from 'rxjs';
+import { DataSourceType, isDataSourceType } from '../../common/data_sources';
+import { addLog } from '../utils/add_log';
+import type {
+  RootProfileService,
+  DataSourceProfileService,
+  DocumentProfileService,
+  RootProfileProviderParams,
+  DataSourceProfileProviderParams,
+  DocumentProfileProviderParams,
+  RootContext,
+  DataSourceContext,
+  DocumentContext,
+} from './profiles';
+import type { ContextWithProfileId } from './profile_service';
+
+interface SerializedRootProfileParams {
+  solutionNavId: RootProfileProviderParams['solutionNavId'];
+}
+
+interface SerializedDataSourceProfileParams {
+  dataViewId: string | undefined;
+  esqlQuery: string | undefined;
+}
+
+interface DataTableRecordWithContext extends DataTableRecord {
+  context: ContextWithProfileId<DocumentContext>;
+}
+
+export interface GetProfilesOptions {
+  record?: DataTableRecord;
+}
+
+export class ProfilesManager {
+  private readonly rootContext$: BehaviorSubject<ContextWithProfileId<RootContext>>;
+  private readonly dataSourceContext$: BehaviorSubject<ContextWithProfileId<DataSourceContext>>;
+
+  private prevRootProfileParams?: SerializedRootProfileParams;
+  private prevDataSourceProfileParams?: SerializedDataSourceProfileParams;
+  private rootProfileAbortController?: AbortController;
+  private dataSourceProfileAbortController?: AbortController;
+
+  constructor(
+    private readonly rootProfileService: RootProfileService,
+    private readonly dataSourceProfileService: DataSourceProfileService,
+    private readonly documentProfileService: DocumentProfileService
+  ) {
+    this.rootContext$ = new BehaviorSubject(rootProfileService.defaultContext);
+    this.dataSourceContext$ = new BehaviorSubject(dataSourceProfileService.defaultContext);
+  }
+
+  public async resolveRootProfile(params: RootProfileProviderParams) {
+    const serializedParams = serializeRootProfileParams(params);
+
+    if (isEqual(this.prevRootProfileParams, serializedParams)) {
+      return;
+    }
+
+    const abortController = new AbortController();
+    this.rootProfileAbortController?.abort();
+    this.rootProfileAbortController = abortController;
+
+    let context = this.rootProfileService.defaultContext;
+
+    try {
+      context = await this.rootProfileService.resolve(params);
+    } catch (e) {
+      logResolutionError(ContextType.Root, serializedParams, e);
+    }
+
+    if (abortController.signal.aborted) {
+      return;
+    }
+
+    this.rootContext$.next(context);
+    this.prevRootProfileParams = serializedParams;
+  }
+
+  public async resolveDataSourceProfile(params: DataSourceProfileProviderParams) {
+    const serializedParams = serializeDataSourceProfileParams(params);
+
+    if (isEqual(this.prevDataSourceProfileParams, serializedParams)) {
+      return;
+    }
+
+    const abortController = new AbortController();
+    this.dataSourceProfileAbortController?.abort();
+    this.dataSourceProfileAbortController = abortController;
+
+    let context = this.dataSourceProfileService.defaultContext;
+
+    try {
+      context = await this.dataSourceProfileService.resolve(params);
+    } catch (e) {
+      logResolutionError(ContextType.DataSource, serializedParams, e);
+    }
+
+    if (abortController.signal.aborted) {
+      return;
+    }
+
+    this.dataSourceContext$.next(context);
+    this.prevDataSourceProfileParams = serializedParams;
+  }
+
+  public resolveDocumentProfile(params: DocumentProfileProviderParams) {
+    let context: ContextWithProfileId<DocumentContext> | undefined;
+
+    return new Proxy(params.record, {
+      has: (target, prop) => prop === 'context' || Reflect.has(target, prop),
+      get: (target, prop, receiver) => {
+        if (prop !== 'context') {
+          return Reflect.get(target, prop, receiver);
+        }
+
+        if (!context) {
+          try {
+            context = this.documentProfileService.resolve(params);
+          } catch (e) {
+            logResolutionError(ContextType.Document, { recordId: params.record.id }, e);
+            context = this.documentProfileService.defaultContext;
+          }
+        }
+
+        return context;
+      },
+    });
+  }
+
+  public getProfiles({ record }: GetProfilesOptions = {}) {
+    return [
+      this.rootProfileService.getProfile(this.rootContext$.getValue()),
+      this.dataSourceProfileService.getProfile(this.dataSourceContext$.getValue()),
+      this.documentProfileService.getProfile(
+        recordHasContext(record) ? record.context : this.documentProfileService.defaultContext
+      ),
+    ];
+  }
+
+  public getProfiles$(options: GetProfilesOptions = {}) {
+    return combineLatest([this.rootContext$, this.dataSourceContext$]).pipe(
+      map(() => this.getProfiles(options))
+    );
+  }
+}
+
+const serializeRootProfileParams = (
+  params: RootProfileProviderParams
+): SerializedRootProfileParams => {
+  return {
+    solutionNavId: params.solutionNavId,
+  };
+};
+
+const serializeDataSourceProfileParams = (
+  params: DataSourceProfileProviderParams
+): SerializedDataSourceProfileParams => {
+  return {
+    dataViewId: isDataSourceType(params.dataSource, DataSourceType.DataView)
+      ? params.dataSource.dataViewId
+      : undefined,
+    esqlQuery:
+      isDataSourceType(params.dataSource, DataSourceType.Esql) &&
+      isOfAggregateQueryType(params.query)
+        ? params.query.esql
+        : undefined,
+  };
+};
+
+const recordHasContext = (
+  record: DataTableRecord | undefined
+): record is DataTableRecordWithContext => {
+  return Boolean(record && 'context' in record);
+};
+
+enum ContextType {
+  Root = 'root',
+  DataSource = 'data source',
+  Document = 'document',
+}
+
+const logResolutionError = <TParams, TError>(
+  profileType: ContextType,
+  params: TParams,
+  error: TError
+) => {
+  addLog(
+    `[ProfilesManager] ${profileType} context resolution failed with params: ${JSON.stringify(
+      params,
+      null,
+      2
+    )}`,
+    error
+  );
+};
diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts
new file mode 100644
index 0000000000000..b612b2ce29907
--- /dev/null
+++ b/src/plugins/discover/public/context_awareness/types.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { CustomCellRenderer } from '@kbn/unified-data-table';
+
+export interface Profile {
+  getCellRenderers: () => CustomCellRenderer;
+}
diff --git a/src/plugins/discover/public/customizations/__mocks__/customization_context.ts b/src/plugins/discover/public/customizations/__mocks__/customization_context.ts
index 6ede54673cda9..1fabe661dd20e 100644
--- a/src/plugins/discover/public/customizations/__mocks__/customization_context.ts
+++ b/src/plugins/discover/public/customizations/__mocks__/customization_context.ts
@@ -6,12 +6,6 @@
  * Side Public License, v 1.
  */
 
-import type { DiscoverCustomizationContext } from '../types';
+import { defaultCustomizationContext } from '../defaults';
 
-export const mockCustomizationContext: DiscoverCustomizationContext = {
-  displayMode: 'standalone',
-  inlineTopNav: {
-    enabled: false,
-    showLogsExplorerTabs: false,
-  },
-};
+export const mockCustomizationContext = defaultCustomizationContext;
diff --git a/src/plugins/discover/public/customizations/defaults.ts b/src/plugins/discover/public/customizations/defaults.ts
index a9dc60ac356ff..034e7be2b5dc6 100644
--- a/src/plugins/discover/public/customizations/defaults.ts
+++ b/src/plugins/discover/public/customizations/defaults.ts
@@ -9,6 +9,7 @@
 import { DiscoverCustomizationContext } from './types';
 
 export const defaultCustomizationContext: DiscoverCustomizationContext = {
+  solutionNavId: null,
   displayMode: 'standalone',
   inlineTopNav: {
     enabled: false,
diff --git a/src/plugins/discover/public/customizations/types.ts b/src/plugins/discover/public/customizations/types.ts
index 079cde37da716..21419da709946 100644
--- a/src/plugins/discover/public/customizations/types.ts
+++ b/src/plugins/discover/public/customizations/types.ts
@@ -21,6 +21,10 @@ export type CustomizationCallback = (
 export type DiscoverDisplayMode = 'embedded' | 'standalone';
 
 export interface DiscoverCustomizationContext {
+  /**
+   * The current solution nav ID
+   */
+  solutionNavId: string | null;
   /*
    * Display mode in which discover is running
    */
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts
index 35e336df52325..53ce8c798f251 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts
@@ -9,6 +9,7 @@
 import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
 import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks';
 import type { DataView } from '@kbn/data-views-plugin/common';
+import { createDataViewDataSource } from '../../common/data_sources';
 import { SHOW_FIELD_STATISTICS } from '@kbn/discover-utils';
 import { buildDataViewMock, deepMockedFields } from '@kbn/discover-utils/src/__mocks__';
 import { ViewMode } from '@kbn/embeddable-plugin/public';
@@ -17,7 +18,7 @@ import { ReactWrapper } from 'enzyme';
 import { ReactElement } from 'react';
 import { render } from 'react-dom';
 import { act } from 'react-dom/test-utils';
-import { Observable, throwError } from 'rxjs';
+import { BehaviorSubject, Observable, throwError } from 'rxjs';
 import { SearchInput } from '..';
 import { VIEW_MODE } from '../../common/constants';
 import { DiscoverServices } from '../build_services';
@@ -26,6 +27,7 @@ import { discoverServiceMock } from '../__mocks__/services';
 import { getDiscoverLocatorParams } from './get_discover_locator_params';
 import { SavedSearchEmbeddable, SearchEmbeddableConfig } from './saved_search_embeddable';
 import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component';
+import { DiscoverGrid } from '../components/discover_grid';
 
 jest.mock('./get_discover_locator_params', () => {
   const actual = jest.requireActual('./get_discover_locator_params');
@@ -140,6 +142,7 @@ describe('saved search embeddable', () => {
   };
 
   beforeEach(() => {
+    jest.clearAllMocks();
     mountpoint = document.createElement('div');
 
     showFieldStatisticsMockValue = false;
@@ -152,6 +155,10 @@ describe('saved search embeddable', () => {
         if (key === SHOW_FIELD_STATISTICS) return showFieldStatisticsMockValue;
       }
     );
+
+    jest
+      .spyOn(servicesMock.core.chrome, 'getActiveSolutionNavId$')
+      .mockReturnValue(new BehaviorSubject('test'));
   });
 
   afterEach(() => {
@@ -475,4 +482,56 @@ describe('saved search embeddable', () => {
       expect(editUrl).toBe('/base/mock-url');
     });
   });
+
+  describe('context awareness', () => {
+    it('should resolve root profile on init', async () => {
+      const resolveRootProfileSpy = jest.spyOn(
+        discoverServiceMock.profilesManager,
+        'resolveRootProfile'
+      );
+      const { embeddable } = createEmbeddable();
+      expect(resolveRootProfileSpy).not.toHaveBeenCalled();
+      await waitOneTick();
+      expect(resolveRootProfileSpy).toHaveBeenCalledWith({ solutionNavId: 'test' });
+      resolveRootProfileSpy.mockReset();
+      expect(resolveRootProfileSpy).not.toHaveBeenCalled();
+      embeddable.reload();
+      await waitOneTick();
+      expect(resolveRootProfileSpy).not.toHaveBeenCalled();
+    });
+
+    it('should resolve data source profile when fetching', async () => {
+      const resolveDataSourceProfileSpy = jest.spyOn(
+        discoverServiceMock.profilesManager,
+        'resolveDataSourceProfile'
+      );
+      const { embeddable } = createEmbeddable();
+      expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled();
+      await waitOneTick();
+      expect(resolveDataSourceProfileSpy).toHaveBeenCalledWith({
+        dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }),
+        dataView: dataViewMock,
+        query: embeddable.getInput().query,
+      });
+      resolveDataSourceProfileSpy.mockReset();
+      expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled();
+      embeddable.reload();
+      expect(resolveDataSourceProfileSpy).toHaveBeenCalledWith({
+        dataSource: createDataViewDataSource({ dataViewId: dataViewMock.id! }),
+        dataView: dataViewMock,
+        query: embeddable.getInput().query,
+      });
+    });
+
+    it('should pass cell renderers from profile', async () => {
+      const { embeddable } = createEmbeddable();
+      await waitOneTick();
+      embeddable.render(mountpoint);
+      const discoverGridComponent = discoverComponent.find(DiscoverGrid);
+      expect(discoverGridComponent.exists()).toBeTruthy();
+      expect(Object.keys(discoverGridComponent.prop('externalCustomRenderers')!)).toEqual([
+        'rootProfile',
+      ]);
+    });
+  });
 });
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
index 3a6f9f9c9c8ac..861a0d50eeba6 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
@@ -6,7 +6,7 @@
  * Side Public License, v 1.
  */
 
-import { lastValueFrom, Subscription } from 'rxjs';
+import { firstValueFrom, lastValueFrom, Subscription } from 'rxjs';
 import {
   onlyDisabledFiltersChanged,
   Filter,
@@ -71,6 +71,7 @@ import { fetchEsql } from '../application/main/data_fetching/fetch_esql';
 import { getValidViewMode } from '../application/main/utils/get_valid_view_mode';
 import { ADHOC_DATA_VIEW_RENDER_EVENT } from '../constants';
 import { getDiscoverLocatorParams } from './get_discover_locator_params';
+import { createDataViewDataSource, createEsqlDataSource } from '../../common/data_sources';
 
 export interface SearchEmbeddableConfig {
   editable: boolean;
@@ -163,6 +164,12 @@ export class SavedSearchEmbeddable
 
       await this.initializeOutput();
 
+      const solutionNavId = await firstValueFrom(
+        this.services.core.chrome.getActiveSolutionNavId$()
+      );
+
+      await this.services.profilesManager.resolveRootProfile({ solutionNavId });
+
       // deferred loading of this embeddable is complete
       this.setInitializationFinished();
 
@@ -305,18 +312,29 @@ export class SavedSearchEmbeddable
     const isEsqlMode = this.isEsqlMode(savedSearch);
 
     try {
+      await this.services.profilesManager.resolveDataSourceProfile({
+        dataSource: isOfAggregateQueryType(query)
+          ? createEsqlDataSource()
+          : dataView.id
+          ? createDataViewDataSource({ dataViewId: dataView.id })
+          : undefined,
+        dataView,
+        query,
+      });
+
       // Request ES|QL data
       if (isEsqlMode && query) {
-        const result = await fetchEsql(
-          savedSearch.searchSource.getField('query')!,
+        const result = await fetchEsql({
+          query: savedSearch.searchSource.getField('query')!,
+          inputQuery: this.input.query,
+          filters: this.input.filters,
           dataView,
-          this.services.data,
-          this.services.expressions,
-          this.services.inspector,
-          this.abortController.signal,
-          this.input.filters,
-          this.input.query
-        );
+          abortSignal: this.abortController.signal,
+          inspectorAdapters: this.services.inspector,
+          data: this.services.data,
+          expressions: this.services.expressions,
+          profilesManager: this.services.profilesManager,
+        });
 
         this.updateOutput({
           ...this.getOutput(),
diff --git a/src/plugins/discover/public/embeddable/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/saved_search_grid.tsx
index 8bf43fa5b3e3b..39a6dc1307c04 100644
--- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx
@@ -21,6 +21,7 @@ import './saved_search_grid.scss';
 import { DiscoverGridFlyout } from '../components/discover_grid_flyout';
 import { SavedSearchEmbeddableBase } from './saved_search_embeddable_base';
 import { TotalDocuments } from '../application/main/components/total_documents/total_documents';
+import { useProfileAccessor } from '../context_awareness';
 
 export interface DiscoverGridEmbeddableProps
   extends Omit<UnifiedDataTableProps, 'sampleSizeState'> {
@@ -88,6 +89,12 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) {
     [props.totalHitCount]
   );
 
+  const getCellRenderersAccessor = useProfileAccessor('getCellRenderers');
+  const cellRenderers = useMemo(() => {
+    const getCellRenderers = getCellRenderersAccessor(() => ({}));
+    return getCellRenderers();
+  }, [getCellRenderersAccessor]);
+
   return (
     <SavedSearchEmbeddableBase
       totalHitCount={undefined} // it will be rendered inside the custom grid toolbar instead
@@ -105,6 +112,7 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) {
         maxDocFieldsDisplayed={props.services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)}
         renderDocumentView={renderDocumentView}
         renderCustomToolbar={renderCustomToolbarWithElements}
+        externalCustomRenderers={cellRenderers}
         enableComparisonMode
         showColumnTokens
         configHeaderRowHeight={3}
diff --git a/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts b/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts
index 29c72391fbf12..6063e0903ffe5 100644
--- a/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts
+++ b/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts
@@ -13,6 +13,7 @@ import { createStartContractMock } from '../__mocks__/start_contract';
 import { discoverServiceMock } from '../__mocks__/services';
 import { ViewMode } from '@kbn/embeddable-plugin/public';
 import { getDiscoverLocatorParams } from './get_discover_locator_params';
+import { BehaviorSubject } from 'rxjs';
 
 const applicationMock = createStartContractMock();
 const services = discoverServiceMock;
@@ -34,6 +35,10 @@ const embeddableConfig = {
   executeTriggerActions,
 };
 
+jest
+  .spyOn(services.core.chrome, 'getActiveSolutionNavId$')
+  .mockReturnValue(new BehaviorSubject('test'));
+
 describe('view saved search action', () => {
   it('is compatible when embeddable is of type saved search, in view mode && appropriate permissions are set', async () => {
     const action = new ViewSavedSearchAction(applicationMock, services.locator);
diff --git a/src/plugins/discover/public/hooks/show_confirm_panel.tsx b/src/plugins/discover/public/hooks/show_confirm_panel.tsx
deleted file mode 100644
index 79d2524c93161..0000000000000
--- a/src/plugins/discover/public/hooks/show_confirm_panel.tsx
+++ /dev/null
@@ -1,71 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import React from 'react';
-import ReactDOM from 'react-dom';
-import { EuiConfirmModal } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
-import { StartRenderServices } from '../plugin';
-
-let isOpenConfirmPanel = false;
-
-export const showConfirmPanel = ({
-  onConfirm,
-  onCancel,
-  startServices,
-}: {
-  onConfirm: () => void;
-  onCancel: () => void;
-  startServices: StartRenderServices;
-}) => {
-  if (isOpenConfirmPanel) {
-    return;
-  }
-
-  isOpenConfirmPanel = true;
-  const container = document.createElement('div');
-  const onClose = () => {
-    ReactDOM.unmountComponentAtNode(container);
-    document.body.removeChild(container);
-    isOpenConfirmPanel = false;
-  };
-
-  document.body.appendChild(container);
-  const element = (
-    <KibanaRenderContextProvider {...startServices}>
-      <EuiConfirmModal
-        title={i18n.translate('discover.confirmDataViewSave.title', {
-          defaultMessage: 'Save data view',
-        })}
-        onCancel={() => {
-          onClose();
-          onCancel();
-        }}
-        onConfirm={() => {
-          onClose();
-          onConfirm();
-        }}
-        cancelButtonText={i18n.translate('discover.confirmDataViewSave.cancel', {
-          defaultMessage: 'Cancel',
-        })}
-        confirmButtonText={i18n.translate('discover.confirmDataViewSave.saveAndContinue', {
-          defaultMessage: 'Save and continue',
-        })}
-        defaultFocusedButton="confirm"
-      >
-        <p>
-          {i18n.translate('discover.confirmDataViewSave.message', {
-            defaultMessage: 'The action you chose requires a saved data view.',
-          })}
-        </p>
-      </EuiConfirmModal>
-    </KibanaRenderContextProvider>
-  );
-  ReactDOM.render(element, container);
-};
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index 2eb34b20345e4..7228070fe2d2c 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -82,6 +82,12 @@ import {
 import { getESQLSearchProvider } from './global_search/search_provider';
 import { HistoryService } from './history_service';
 import { ConfigSchema, ExperimentalFeatures } from '../common/config';
+import {
+  DataSourceProfileService,
+  DocumentProfileService,
+  ProfilesManager,
+  RootProfileService,
+} from './context_awareness';
 
 /**
  * @public
@@ -209,8 +215,6 @@ export interface DiscoverStartPlugins {
   observabilityAIAssistant?: ObservabilityAIAssistantPublicStart;
 }
 
-export type StartRenderServices = Pick<CoreStart, 'analytics' | 'i18n' | 'theme'>;
-
 /**
  * Contains Discover, one of the oldest parts of Kibana
  * Discover provides embeddables for Dashboards
@@ -218,25 +222,28 @@ export type StartRenderServices = Pick<CoreStart, 'analytics' | 'i18n' | 'theme'
 export class DiscoverPlugin
   implements Plugin<DiscoverSetup, DiscoverStart, DiscoverSetupPlugins, DiscoverStartPlugins>
 {
-  constructor(private readonly initializerContext: PluginInitializerContext<ConfigSchema>) {
-    this.experimentalFeatures =
-      initializerContext.config.get().experimental ?? this.experimentalFeatures;
-  }
+  private readonly rootProfileService = new RootProfileService();
+  private readonly dataSourceProfileService = new DataSourceProfileService();
+  private readonly documentProfileService = new DocumentProfileService();
+  private readonly appStateUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
+  private readonly historyService = new HistoryService();
+  private readonly inlineTopNav: Map<string | null, DiscoverCustomizationContext['inlineTopNav']> =
+    new Map([[null, defaultCustomizationContext.inlineTopNav]]);
+  private readonly experimentalFeatures: ExperimentalFeatures = {
+    ruleFormV2Enabled: false,
+  };
 
-  private appStateUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
-  private historyService = new HistoryService();
   private scopedHistory?: ScopedHistory<unknown>;
   private urlTracker?: UrlTracker;
-  private stopUrlTracking: (() => void) | undefined = undefined;
+  private stopUrlTracking?: () => void;
   private locator?: DiscoverAppLocator;
   private contextLocator?: DiscoverContextAppLocator;
   private singleDocLocator?: DiscoverSingleDocLocator;
-  private inlineTopNav: Map<string | null, DiscoverCustomizationContext['inlineTopNav']> = new Map([
-    [null, defaultCustomizationContext.inlineTopNav],
-  ]);
-  private experimentalFeatures: ExperimentalFeatures = {
-    ruleFormV2Enabled: false,
-  };
+
+  constructor(private readonly initializerContext: PluginInitializerContext<ConfigSchema>) {
+    this.experimentalFeatures =
+      initializerContext.config.get().experimental ?? this.experimentalFeatures;
+  }
 
   setup(
     core: CoreSetup<DiscoverStartPlugins, DiscoverStart>,
@@ -331,6 +338,7 @@ export class DiscoverPlugin
           history: this.historyService.getHistory(),
           scopedHistory: this.scopedHistory,
           urlTracker: this.urlTracker!,
+          profilesManager: this.createProfilesManager(),
           setHeaderActionMenu: params.setHeaderActionMenu,
         });
 
@@ -344,10 +352,11 @@ export class DiscoverPlugin
         const customizationContext$: Observable<DiscoverCustomizationContext> = services.chrome
           .getActiveSolutionNavId$()
           .pipe(
-            map((navId) => ({
+            map((solutionNavId) => ({
               ...defaultCustomizationContext,
+              solutionNavId,
               inlineTopNav:
-                this.inlineTopNav.get(navId) ??
+                this.inlineTopNav.get(solutionNavId) ??
                 this.inlineTopNav.get(null) ??
                 defaultCustomizationContext.inlineTopNav,
             }))
@@ -412,10 +421,7 @@ export class DiscoverPlugin
   }
 
   start(core: CoreStart, plugins: DiscoverStartPlugins): DiscoverStart {
-    // we need to register the application service at setup, but to render it
-    // there are some start dependencies necessary, for this reason
-    // initializeServices are assigned at start and used
-    // when the application/embeddable is mounted
+    this.registerProfiles();
 
     const viewSavedSearchAction = new ViewSavedSearchAction(core.application, this.locator!);
 
@@ -423,7 +429,6 @@ export class DiscoverPlugin
     plugins.uiActions.registerTrigger(SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER);
     injectTruncateStyles(core.uiSettings.get(TRUNCATE_MAX_HEIGHT));
 
-    const getDiscoverServicesInternal = () => this.getDiscoverServices(core, plugins);
     const isEsqlEnabled = core.uiSettings.get(ENABLE_ESQL);
 
     if (plugins.share && this.locator && isEsqlEnabled) {
@@ -435,6 +440,10 @@ export class DiscoverPlugin
       );
     }
 
+    const getDiscoverServicesInternal = () => {
+      return this.getDiscoverServices(core, plugins, this.createEmptyProfilesManager());
+    };
+
     return {
       locator: this.locator,
       DiscoverContainer: (props: DiscoverContainerProps) => (
@@ -449,7 +458,34 @@ export class DiscoverPlugin
     }
   }
 
-  private getDiscoverServices = (core: CoreStart, plugins: DiscoverStartPlugins) => {
+  private registerProfiles() {
+    // TODO: Conditionally register example profiles for functional testing in a follow up PR
+    // this.rootProfileService.registerProvider(o11yRootProfileProvider);
+    // this.dataSourceProfileService.registerProvider(logsDataSourceProfileProvider);
+    // this.documentProfileService.registerProvider(logDocumentProfileProvider);
+  }
+
+  private createProfilesManager() {
+    return new ProfilesManager(
+      this.rootProfileService,
+      this.dataSourceProfileService,
+      this.documentProfileService
+    );
+  }
+
+  private createEmptyProfilesManager() {
+    return new ProfilesManager(
+      new RootProfileService(),
+      new DataSourceProfileService(),
+      new DocumentProfileService()
+    );
+  }
+
+  private getDiscoverServices = (
+    core: CoreStart,
+    plugins: DiscoverStartPlugins,
+    profilesManager = this.createProfilesManager()
+  ) => {
     return buildServices({
       core,
       plugins,
@@ -459,6 +495,7 @@ export class DiscoverPlugin
       singleDocLocator: this.singleDocLocator!,
       history: this.historyService.getHistory(),
       urlTracker: this.urlTracker!,
+      profilesManager,
     });
   };
 
diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx
index 465f1f3f66eff..484af6e4a0809 100644
--- a/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx
+++ b/x-pack/plugins/observability_solution/logs_shared/public/components/log_ai_assistant/index.tsx
@@ -22,12 +22,12 @@ export function createLogAIAssistant({
 export const createLogsAIAssistantRenderer =
   (LogAIAssistantRender: ReturnType<typeof createLogAIAssistant>) =>
   ({ doc }: ObservabilityLogsAIAssistantFeatureRenderDeps) => {
-    const mappedDoc = useMemo(
+    const mappedDoc = useMemo<LogAIAssistantDocument>(
       () => ({
         fields: Object.entries(doc.flattened).map(([field, value]) => ({
           field,
-          value,
-        })) as LogAIAssistantDocument['fields'],
+          value: Array.isArray(value) ? value : [value],
+        })),
       }),
       [doc]
     );
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 09336d56965fa..f4067130e1346 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -2322,10 +2322,6 @@
     "discover.backToTopLinkText": "Revenir en haut de la page.",
     "discover.badge.readOnly.text": "Lecture seule",
     "discover.badge.readOnly.tooltip": "Impossible d’enregistrer les recherches",
-    "discover.confirmDataViewSave.cancel": "Annuler",
-    "discover.confirmDataViewSave.message": "L'action que vous avez choisie requiert une vue de données enregistrée.",
-    "discover.confirmDataViewSave.saveAndContinue": "Enregistrer et continuer",
-    "discover.confirmDataViewSave.title": "Enregistrer la vue de données",
     "discover.context.breadcrumb": "Documents relatifs",
     "discover.context.failedToLoadAnchorDocumentDescription": "Échec de chargement du document ancré",
     "discover.context.failedToLoadAnchorDocumentErrorDescription": "Le document ancré n’a pas pu être chargé.",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 55589d731e969..f18f09518aa37 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -2319,10 +2319,6 @@
     "discover.backToTopLinkText": "最上部へ戻る。",
     "discover.badge.readOnly.text": "読み取り専用",
     "discover.badge.readOnly.tooltip": "検索を保存できません",
-    "discover.confirmDataViewSave.cancel": "キャンセル",
-    "discover.confirmDataViewSave.message": "選択したアクションでは、保存されたデータビューが必要です。",
-    "discover.confirmDataViewSave.saveAndContinue": "保存して続行",
-    "discover.confirmDataViewSave.title": "データビューを保存",
     "discover.context.breadcrumb": "周りのドキュメント",
     "discover.context.failedToLoadAnchorDocumentDescription": "アンカードキュメントの読み込みに失敗しました",
     "discover.context.failedToLoadAnchorDocumentErrorDescription": "アンカードキュメントの読み込みに失敗しました。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 65cf2dad16d19..90699177a60b4 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -2323,10 +2323,6 @@
     "discover.backToTopLinkText": "返回顶部。",
     "discover.badge.readOnly.text": "只读",
     "discover.badge.readOnly.tooltip": "无法保存搜索",
-    "discover.confirmDataViewSave.cancel": "取消",
-    "discover.confirmDataViewSave.message": "您选择的操作需要已保存的数据视图。",
-    "discover.confirmDataViewSave.saveAndContinue": "保存并继续",
-    "discover.confirmDataViewSave.title": "保存数据视图。",
     "discover.context.breadcrumb": "周围文档",
     "discover.context.failedToLoadAnchorDocumentDescription": "无法加载定位点文档",
     "discover.context.failedToLoadAnchorDocumentErrorDescription": "无法加载定位点文档。",