diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json
index 771c19cfdbd3d..0ac40ae1889de 100644
--- a/examples/embeddable_examples/kibana.json
+++ b/examples/embeddable_examples/kibana.json
@@ -4,7 +4,7 @@
   "kibanaVersion": "kibana",
   "server": true,
   "ui": true,
-  "requiredPlugins": ["embeddable", "uiActions"],
+  "requiredPlugins": ["embeddable", "uiActions", "dashboard"],
   "optionalPlugins": [],
   "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"],
   "requiredBundles": ["kibanaReact"]
diff --git a/examples/embeddable_examples/public/book/add_book_to_library_action.tsx b/examples/embeddable_examples/public/book/add_book_to_library_action.tsx
new file mode 100644
index 0000000000000..b74a1d5642982
--- /dev/null
+++ b/examples/embeddable_examples/public/book/add_book_to_library_action.tsx
@@ -0,0 +1,55 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { createAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public';
+import { BookEmbeddable, BOOK_EMBEDDABLE } from './book_embeddable';
+import { ViewMode, isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public';
+
+interface ActionContext {
+  embeddable: BookEmbeddable;
+}
+
+export const ACTION_ADD_BOOK_TO_LIBRARY = 'ACTION_ADD_BOOK_TO_LIBRARY';
+
+export const createAddBookToLibraryAction = () =>
+  createAction({
+    getDisplayName: () =>
+      i18n.translate('embeddableExamples.book.addToLibrary', {
+        defaultMessage: 'Add Book To Library',
+      }),
+    type: ACTION_ADD_BOOK_TO_LIBRARY,
+    order: 100,
+    getIconType: () => 'folderCheck',
+    isCompatible: async ({ embeddable }: ActionContext) => {
+      return (
+        embeddable.type === BOOK_EMBEDDABLE &&
+        embeddable.getInput().viewMode === ViewMode.EDIT &&
+        isReferenceOrValueEmbeddable(embeddable) &&
+        !embeddable.inputIsRefType(embeddable.getInput())
+      );
+    },
+    execute: async ({ embeddable }: ActionContext) => {
+      if (!isReferenceOrValueEmbeddable(embeddable)) {
+        throw new IncompatibleActionError();
+      }
+      const newInput = await embeddable.getInputAsRefType();
+      embeddable.updateInput(newInput);
+    },
+  });
diff --git a/examples/embeddable_examples/public/book/book_component.tsx b/examples/embeddable_examples/public/book/book_component.tsx
index 064e13c131a0a..e46487641b913 100644
--- a/examples/embeddable_examples/public/book/book_component.tsx
+++ b/examples/embeddable_examples/public/book/book_component.tsx
@@ -20,7 +20,7 @@ import React from 'react';
 import { EuiFlexItem, EuiFlexGroup, EuiIcon } from '@elastic/eui';
 
 import { EuiText } from '@elastic/eui';
-import { EuiFlexGrid } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
 import { withEmbeddableSubscription } from '../../../../src/plugins/embeddable/public';
 import { BookEmbeddableInput, BookEmbeddableOutput, BookEmbeddable } from './book_embeddable';
 
@@ -44,26 +44,32 @@ function wrapSearchTerms(task?: string, search?: string) {
   );
 }
 
-export function BookEmbeddableComponentInner({ input: { search }, output: { attributes } }: Props) {
+export function BookEmbeddableComponentInner({
+  input: { search },
+  output: { attributes },
+  embeddable,
+}: Props) {
   const title = attributes?.title;
   const author = attributes?.author;
   const readIt = attributes?.readIt;
 
+  const byReference = embeddable.inputIsRefType(embeddable.getInput());
+
   return (
     <EuiFlexGroup gutterSize="s">
       <EuiFlexItem>
-        <EuiFlexGrid columns={1} gutterSize="none">
+        <EuiFlexGroup direction="column" gutterSize="s">
           {title ? (
             <EuiFlexItem>
               <EuiText data-test-subj="bookEmbeddableTitle">
-                <h3>{wrapSearchTerms(title, search)},</h3>
+                <h3>{wrapSearchTerms(title, search)}</h3>
               </EuiText>
             </EuiFlexItem>
           ) : null}
           {author ? (
             <EuiFlexItem>
               <EuiText data-test-subj="bookEmbeddableAuthor">
-                <h5>-{wrapSearchTerms(author, search)}</h5>
+                -{wrapSearchTerms(author, search)}
               </EuiText>
             </EuiFlexItem>
           ) : null}
@@ -76,7 +82,21 @@ export function BookEmbeddableComponentInner({ input: { search }, output: { attr
               <EuiIcon type="cross" />
             </EuiFlexItem>
           )}
-        </EuiFlexGrid>
+        </EuiFlexGroup>
+      </EuiFlexItem>
+      <EuiFlexItem>
+        <EuiText data-test-subj="bookEmbeddableAuthor">
+          <EuiIcon type={byReference ? 'folderCheck' : 'folderExclamation'} />{' '}
+          <span>
+            {byReference
+              ? i18n.translate('embeddableExamples.book.byReferenceLabel', {
+                  defaultMessage: 'Book is By Reference',
+                })
+              : i18n.translate('embeddableExamples.book.byValueLabel', {
+                  defaultMessage: 'Book is By Value',
+                })}
+          </span>
+        </EuiText>
       </EuiFlexItem>
     </EuiFlexGroup>
   );
diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx
index d49bd3280d97d..dd9418c0e8596 100644
--- a/examples/embeddable_examples/public/book/book_embeddable.tsx
+++ b/examples/embeddable_examples/public/book/book_embeddable.tsx
@@ -25,10 +25,11 @@ import {
   IContainer,
   EmbeddableOutput,
   SavedObjectEmbeddableInput,
-  AttributeService,
+  ReferenceOrValueEmbeddable,
 } from '../../../../src/plugins/embeddable/public';
 import { BookSavedObjectAttributes } from '../../common';
 import { BookEmbeddableComponent } from './book_component';
+import { AttributeService } from '../../../../src/plugins/dashboard/public';
 
 export const BOOK_EMBEDDABLE = 'book';
 export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput;
@@ -59,7 +60,8 @@ function getHasMatch(search?: string, savedAttributes?: BookSavedObjectAttribute
   );
 }
 
-export class BookEmbeddable extends Embeddable<BookEmbeddableInput, BookEmbeddableOutput> {
+export class BookEmbeddable extends Embeddable<BookEmbeddableInput, BookEmbeddableOutput>
+  implements ReferenceOrValueEmbeddable<BookByValueInput, BookByReferenceInput> {
   public readonly type = BOOK_EMBEDDABLE;
   private subscription: Subscription;
   private node?: HTMLElement;
@@ -96,6 +98,18 @@ export class BookEmbeddable extends Embeddable<BookEmbeddableInput, BookEmbeddab
     });
   }
 
+  inputIsRefType = (input: BookEmbeddableInput): input is BookByReferenceInput => {
+    return this.attributeService.inputIsRefType(input);
+  };
+
+  getInputAsValueType = async (): Promise<BookByValueInput> => {
+    return this.attributeService.getInputAsValueType(this.input);
+  };
+
+  getInputAsRefType = async (): Promise<BookByReferenceInput> => {
+    return this.attributeService.getInputAsRefType(this.input, { showSaveModal: true });
+  };
+
   public render(node: HTMLElement) {
     if (this.node) {
       ReactDOM.unmountComponentAtNode(this.node);
@@ -113,6 +127,10 @@ export class BookEmbeddable extends Embeddable<BookEmbeddableInput, BookEmbeddab
     });
   }
 
+  public getTitle() {
+    return this.getOutput()?.title || this.getOutput().attributes?.title;
+  }
+
   public destroy() {
     super.destroy();
     this.subscription.unsubscribe();
diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx
index f4a32fb498a2d..4c144c3843c47 100644
--- a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx
+++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx
@@ -23,9 +23,7 @@ import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common';
 import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
 import {
   EmbeddableFactoryDefinition,
-  EmbeddableStart,
   IContainer,
-  AttributeService,
   EmbeddableFactory,
 } from '../../../../src/plugins/embeddable/public';
 import {
@@ -38,9 +36,10 @@ import {
 } from './book_embeddable';
 import { CreateEditBookComponent } from './create_edit_book_component';
 import { OverlayStart } from '../../../../src/core/public';
+import { DashboardStart, AttributeService } from '../../../../src/plugins/dashboard/public';
 
 interface StartServices {
-  getAttributeService: EmbeddableStart['getAttributeService'];
+  getAttributeService: DashboardStart['getAttributeService'];
   openModal: OverlayStart['openModal'];
 }
 
@@ -85,6 +84,16 @@ export class BookEmbeddableFactoryDefinition
     });
   }
 
+  // This is currently required due to the distinction in container.ts and the
+  // default error implementation in default_embeddable_factory_provider.ts
+  public async createFromSavedObject(
+    savedObjectId: string,
+    input: BookEmbeddableInput,
+    parent?: IContainer
+  ) {
+    return this.create(input, parent);
+  }
+
   public getDisplayName() {
     return i18n.translate('embeddableExamples.book.displayName', {
       defaultMessage: 'Book',
@@ -122,6 +131,6 @@ export class BookEmbeddableFactoryDefinition
         BookByReferenceInput
       >(this.type);
     }
-    return this.attributeService;
+    return this.attributeService!;
   }
 }
diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx
index 222f70e0be60f..b31d69696598e 100644
--- a/examples/embeddable_examples/public/book/edit_book_action.tsx
+++ b/examples/embeddable_examples/public/book/edit_book_action.tsx
@@ -22,11 +22,7 @@ import { i18n } from '@kbn/i18n';
 import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common';
 import { createAction } from '../../../../src/plugins/ui_actions/public';
 import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
-import {
-  ViewMode,
-  EmbeddableStart,
-  SavedObjectEmbeddableInput,
-} from '../../../../src/plugins/embeddable/public';
+import { ViewMode, SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/public';
 import {
   BookEmbeddable,
   BOOK_EMBEDDABLE,
@@ -34,10 +30,11 @@ import {
   BookByValueInput,
 } from './book_embeddable';
 import { CreateEditBookComponent } from './create_edit_book_component';
+import { DashboardStart } from '../../../../src/plugins/dashboard/public';
 
 interface StartServices {
   openModal: OverlayStart['openModal'];
-  getAttributeService: EmbeddableStart['getAttributeService'];
+  getAttributeService: DashboardStart['getAttributeService'];
 }
 
 interface ActionContext {
diff --git a/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx b/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx
new file mode 100644
index 0000000000000..cef77092a642a
--- /dev/null
+++ b/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx
@@ -0,0 +1,55 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { createAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public';
+import { BookEmbeddable, BOOK_EMBEDDABLE } from './book_embeddable';
+import { ViewMode, isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public';
+
+interface ActionContext {
+  embeddable: BookEmbeddable;
+}
+
+export const ACTION_UNLINK_BOOK_FROM_LIBRARY = 'ACTION_UNLINK_BOOK_FROM_LIBRARY';
+
+export const createUnlinkBookFromLibraryAction = () =>
+  createAction({
+    getDisplayName: () =>
+      i18n.translate('embeddableExamples.book.unlinkFromLibrary', {
+        defaultMessage: 'Unlink Book from Library Item',
+      }),
+    type: ACTION_UNLINK_BOOK_FROM_LIBRARY,
+    order: 100,
+    getIconType: () => 'folderExclamation',
+    isCompatible: async ({ embeddable }: ActionContext) => {
+      return (
+        embeddable.type === BOOK_EMBEDDABLE &&
+        embeddable.getInput().viewMode === ViewMode.EDIT &&
+        isReferenceOrValueEmbeddable(embeddable) &&
+        embeddable.inputIsRefType(embeddable.getInput())
+      );
+    },
+    execute: async ({ embeddable }: ActionContext) => {
+      if (!isReferenceOrValueEmbeddable(embeddable)) {
+        throw new IncompatibleActionError();
+      }
+      const newInput = await embeddable.getInputAsValueType();
+      embeddable.updateInput(newInput);
+    },
+  });
diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts
index 95f4f5b41e198..0c6ed1eb3be48 100644
--- a/examples/embeddable_examples/public/plugin.ts
+++ b/examples/embeddable_examples/public/plugin.ts
@@ -58,6 +58,15 @@ import {
   BookEmbeddableFactoryDefinition,
 } from './book/book_embeddable_factory';
 import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
+import {
+  ACTION_ADD_BOOK_TO_LIBRARY,
+  createAddBookToLibraryAction,
+} from './book/add_book_to_library_action';
+import { DashboardStart } from '../../../src/plugins/dashboard/public';
+import {
+  ACTION_UNLINK_BOOK_FROM_LIBRARY,
+  createUnlinkBookFromLibraryAction,
+} from './book/unlink_book_from_library_action';
 
 export interface EmbeddableExamplesSetupDependencies {
   embeddable: EmbeddableSetup;
@@ -66,6 +75,7 @@ export interface EmbeddableExamplesSetupDependencies {
 
 export interface EmbeddableExamplesStartDependencies {
   embeddable: EmbeddableStart;
+  dashboard: DashboardStart;
 }
 
 interface ExampleEmbeddableFactories {
@@ -86,6 +96,8 @@ export interface EmbeddableExamplesStart {
 declare module '../../../src/plugins/ui_actions/public' {
   export interface ActionContextMapping {
     [ACTION_EDIT_BOOK]: { embeddable: BookEmbeddable };
+    [ACTION_ADD_BOOK_TO_LIBRARY]: { embeddable: BookEmbeddable };
+    [ACTION_UNLINK_BOOK_FROM_LIBRARY]: { embeddable: BookEmbeddable };
   }
 }
 
@@ -144,17 +156,25 @@ export class EmbeddableExamplesPlugin
     this.exampleEmbeddableFactories.getBookEmbeddableFactory = deps.embeddable.registerEmbeddableFactory(
       BOOK_EMBEDDABLE,
       new BookEmbeddableFactoryDefinition(async () => ({
-        getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService,
+        getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService,
         openModal: (await core.getStartServices())[0].overlays.openModal,
       }))
     );
 
     const editBookAction = createEditBookAction(async () => ({
-      getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService,
+      getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService,
       openModal: (await core.getStartServices())[0].overlays.openModal,
     }));
     deps.uiActions.registerAction(editBookAction);
     deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, editBookAction.id);
+
+    const addBookToLibraryAction = createAddBookToLibraryAction();
+    deps.uiActions.registerAction(addBookToLibraryAction);
+    deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, addBookToLibraryAction.id);
+
+    const unlinkBookFromLibraryAction = createUnlinkBookFromLibraryAction();
+    deps.uiActions.registerAction(unlinkBookFromLibraryAction);
+    deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkBookFromLibraryAction.id);
   }
 
   public start(
diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx
new file mode 100644
index 0000000000000..c2f529fe399f3
--- /dev/null
+++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx
@@ -0,0 +1,156 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+  EmbeddableInput,
+  SavedObjectEmbeddableInput,
+  isSavedObjectEmbeddableInput,
+  IEmbeddable,
+} from '../embeddable_plugin';
+import {
+  SavedObjectsClientContract,
+  SimpleSavedObject,
+  I18nStart,
+  NotificationsStart,
+} from '../../../../core/public';
+import {
+  SavedObjectSaveModal,
+  showSaveModal,
+  OnSaveProps,
+  SaveResult,
+} from '../../../saved_objects/public';
+
+/**
+ * The attribute service is a shared, generic service that embeddables can use to provide the functionality
+ * required to fulfill the requirements of the ReferenceOrValueEmbeddable interface. The attribute_service
+ * can also be used as a higher level wrapper to transform an embeddable input shape that references a saved object
+ * into an embeddable input shape that contains that saved object's attributes by value.
+ */
+export class AttributeService<
+  SavedObjectAttributes extends { title: string },
+  ValType extends EmbeddableInput & { attributes: SavedObjectAttributes },
+  RefType extends SavedObjectEmbeddableInput
+> {
+  constructor(
+    private type: string,
+    private savedObjectsClient: SavedObjectsClientContract,
+    private i18nContext: I18nStart['Context'],
+    private toasts: NotificationsStart['toasts']
+  ) {}
+
+  public async unwrapAttributes(input: RefType | ValType): Promise<SavedObjectAttributes> {
+    if (this.inputIsRefType(input)) {
+      const savedObject: SimpleSavedObject<SavedObjectAttributes> = await this.savedObjectsClient.get<
+        SavedObjectAttributes
+      >(this.type, input.savedObjectId);
+      return savedObject.attributes;
+    }
+    return input.attributes;
+  }
+
+  public async wrapAttributes(
+    newAttributes: SavedObjectAttributes,
+    useRefType: boolean,
+    embeddable?: IEmbeddable
+  ): Promise<Omit<ValType | RefType, 'id'>> {
+    const savedObjectId =
+      embeddable && isSavedObjectEmbeddableInput(embeddable.getInput())
+        ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId
+        : undefined;
+    if (!useRefType) {
+      return { attributes: newAttributes } as ValType;
+    } else {
+      try {
+        if (savedObjectId) {
+          await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes);
+          return { savedObjectId } as RefType;
+        } else {
+          const savedItem = await this.savedObjectsClient.create(this.type, newAttributes);
+          return { savedObjectId: savedItem.id } as RefType;
+        }
+      } catch (error) {
+        this.toasts.addDanger({
+          title: i18n.translate('dashboard.attributeService.saveToLibraryError', {
+            defaultMessage: `Panel was not saved to the library. Error: {errorMessage}`,
+            values: {
+              errorMessage: error.message,
+            },
+          }),
+          'data-test-subj': 'saveDashboardFailure',
+        });
+        return Promise.reject({ error });
+      }
+    }
+  }
+
+  inputIsRefType = (input: ValType | RefType): input is RefType => {
+    return isSavedObjectEmbeddableInput(input);
+  };
+
+  getInputAsValueType = async (input: ValType | RefType): Promise<ValType> => {
+    if (!this.inputIsRefType(input)) {
+      return input;
+    }
+    const attributes = await this.unwrapAttributes(input);
+    return {
+      ...input,
+      savedObjectId: undefined,
+      attributes,
+    };
+  };
+
+  getInputAsRefType = async (
+    input: ValType | RefType,
+    saveOptions?: { showSaveModal: boolean } | { title: string }
+  ): Promise<RefType> => {
+    if (this.inputIsRefType(input)) {
+      return input;
+    }
+
+    return new Promise<RefType>((resolve, reject) => {
+      const onSave = async (props: OnSaveProps): Promise<SaveResult> => {
+        try {
+          input.attributes.title = props.newTitle;
+          const wrappedInput = (await this.wrapAttributes(input.attributes, true)) as RefType;
+          resolve(wrappedInput);
+          return { id: wrappedInput.savedObjectId };
+        } catch (error) {
+          reject();
+          return { error };
+        }
+      };
+
+      if (saveOptions && (saveOptions as { showSaveModal: boolean }).showSaveModal) {
+        showSaveModal(
+          <SavedObjectSaveModal
+            onSave={onSave}
+            onClose={() => reject()}
+            title={input.attributes.title}
+            showCopyOnSave={false}
+            objectType={this.type}
+            showDescription={false}
+          />,
+          this.i18nContext
+        );
+      }
+    });
+  };
+}
diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts
index dcfde67cd9f13..8a9954cc77a2e 100644
--- a/src/plugins/dashboard/public/index.ts
+++ b/src/plugins/dashboard/public/index.ts
@@ -40,6 +40,7 @@ export {
 export { addEmbeddableToDashboardUrl } from './url_utils/url_helper';
 export { SavedObjectDashboard } from './saved_dashboards';
 export { SavedDashboardPanel } from './types';
+export { AttributeService } from './attribute_service/attribute_service';
 
 export function plugin(initializerContext: PluginInitializerContext) {
   return new DashboardPlugin(initializerContext);
diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx
index f1319665d258b..3b0863a9f4651 100644
--- a/src/plugins/dashboard/public/plugin.tsx
+++ b/src/plugins/dashboard/public/plugin.tsx
@@ -34,7 +34,13 @@ import {
   ScopedHistory,
 } from 'src/core/public';
 import { UsageCollectionSetup } from '../../usage_collection/public';
-import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from '../../embeddable/public';
+import {
+  CONTEXT_MENU_TRIGGER,
+  EmbeddableSetup,
+  EmbeddableStart,
+  SavedObjectEmbeddableInput,
+  EmbeddableInput,
+} from '../../embeddable/public';
 import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from '../../data/public';
 import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from '../../share/public';
 import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public';
@@ -85,6 +91,7 @@ import { DashboardConstants } from './dashboard_constants';
 import { addEmbeddableToDashboardUrl } from './url_utils/url_helper';
 import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder';
 import { UrlGeneratorState } from '../../share/public';
+import { AttributeService } from '.';
 
 declare module '../../share/public' {
   export interface UrlGeneratorStateMapping {
@@ -131,6 +138,13 @@ export interface DashboardStart {
   dashboardUrlGenerator?: DashboardUrlGenerator;
   dashboardFeatureFlagConfig: DashboardFeatureFlagConfig;
   DashboardContainerByValueRenderer: ReturnType<typeof createDashboardContainerByValueRenderer>;
+  getAttributeService: <
+    A extends { title: string },
+    V extends EmbeddableInput & { attributes: A },
+    R extends SavedObjectEmbeddableInput
+  >(
+    type: string
+  ) => AttributeService<A, V, R>;
 }
 
 declare module '../../../plugins/ui_actions/public' {
@@ -420,6 +434,13 @@ export class DashboardPlugin
       DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({
         factory: dashboardContainerFactory,
       }),
+      getAttributeService: (type: string) =>
+        new AttributeService(
+          type,
+          core.savedObjects.client,
+          core.i18n.Context,
+          core.notifications.toasts
+        ),
     };
   }
 
diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts
index fafbdda148de8..57253c1f741ab 100644
--- a/src/plugins/embeddable/public/index.ts
+++ b/src/plugins/embeddable/public/index.ts
@@ -28,7 +28,8 @@ export {
   ACTION_EDIT_PANEL,
   Adapters,
   AddPanelAction,
-  AttributeService,
+  ReferenceOrValueEmbeddable,
+  isReferenceOrValueEmbeddable,
   ChartActionContext,
   Container,
   ContainerInput,
diff --git a/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts b/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts
deleted file mode 100644
index a33f592350d9a..0000000000000
--- a/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { SavedObjectsClientContract } from '../../../../../core/public';
-import {
-  SavedObjectEmbeddableInput,
-  isSavedObjectEmbeddableInput,
-  EmbeddableInput,
-  IEmbeddable,
-} from '.';
-import { SimpleSavedObject } from '../../../../../core/public';
-
-export class AttributeService<
-  SavedObjectAttributes,
-  ValType extends EmbeddableInput & { attributes: SavedObjectAttributes },
-  RefType extends SavedObjectEmbeddableInput
-> {
-  constructor(private type: string, private savedObjectsClient: SavedObjectsClientContract) {}
-
-  public async unwrapAttributes(input: RefType | ValType): Promise<SavedObjectAttributes> {
-    if (isSavedObjectEmbeddableInput(input)) {
-      const savedObject: SimpleSavedObject<SavedObjectAttributes> = await this.savedObjectsClient.get<
-        SavedObjectAttributes
-      >(this.type, input.savedObjectId);
-      return savedObject.attributes;
-    }
-    return input.attributes;
-  }
-
-  public async wrapAttributes(
-    newAttributes: SavedObjectAttributes,
-    useRefType: boolean,
-    embeddable?: IEmbeddable
-  ): Promise<Omit<ValType | RefType, 'id'>> {
-    const savedObjectId =
-      embeddable && isSavedObjectEmbeddableInput(embeddable.getInput())
-        ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId
-        : undefined;
-
-    if (useRefType) {
-      if (savedObjectId) {
-        await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes);
-        return { savedObjectId } as RefType;
-      } else {
-        const savedItem = await this.savedObjectsClient.create(this.type, newAttributes);
-        return { savedObjectId: savedItem.id } as RefType;
-      }
-    } else {
-      return { attributes: newAttributes } as ValType;
-    }
-  }
-}
diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts
index 06cb6e322acf3..5bab5ac27f3cc 100644
--- a/src/plugins/embeddable/public/lib/embeddables/index.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/index.ts
@@ -25,5 +25,4 @@ export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable';
 export { withEmbeddableSubscription } from './with_subscription';
 export { EmbeddableRoot } from './embeddable_root';
 export * from './saved_object_embeddable';
-export { AttributeService } from './attribute_service';
 export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer';
diff --git a/src/plugins/embeddable/public/lib/index.ts b/src/plugins/embeddable/public/lib/index.ts
index b757fa59a7f3a..aef4c33ee1078 100644
--- a/src/plugins/embeddable/public/lib/index.ts
+++ b/src/plugins/embeddable/public/lib/index.ts
@@ -25,3 +25,4 @@ export * from './triggers';
 export * from './containers';
 export * from './panel';
 export * from './state_transfer';
+export * from './reference_or_value_embeddable';
diff --git a/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts
new file mode 100644
index 0000000000000..e9b8521a35ba5
--- /dev/null
+++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { ReferenceOrValueEmbeddable, isReferenceOrValueEmbeddable } from './types';
diff --git a/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts
new file mode 100644
index 0000000000000..eaf5c94a09138
--- /dev/null
+++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts
@@ -0,0 +1,56 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { EmbeddableInput, SavedObjectEmbeddableInput } from '..';
+
+/**
+ * Any embeddable that implements this interface will be able to use input that is
+ * either by reference (backed by a saved object) OR by value, (provided
+ * by the container).
+ * @public
+ */
+export interface ReferenceOrValueEmbeddable<
+  ValTypeInput extends EmbeddableInput = EmbeddableInput,
+  RefTypeInput extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
+> {
+  /**
+   * determines whether the input is by value or by reference.
+   */
+  inputIsRefType: (input: ValTypeInput | RefTypeInput) => input is RefTypeInput;
+
+  /**
+   * Gets the embeddable's current input as its Value type
+   */
+  getInputAsValueType: () => Promise<ValTypeInput>;
+
+  /**
+   * Gets the embeddable's current input as its Reference type
+   */
+  getInputAsRefType: () => Promise<RefTypeInput>;
+}
+
+export function isReferenceOrValueEmbeddable(
+  incoming: unknown
+): incoming is ReferenceOrValueEmbeddable {
+  return (
+    !!(incoming as ReferenceOrValueEmbeddable).inputIsRefType &&
+    !!(incoming as ReferenceOrValueEmbeddable).getInputAsValueType &&
+    !!(incoming as ReferenceOrValueEmbeddable).getInputAsRefType
+  );
+}
diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx
index 94aa980e446ca..fa79af909a427 100644
--- a/src/plugins/embeddable/public/mocks.tsx
+++ b/src/plugins/embeddable/public/mocks.tsx
@@ -97,7 +97,6 @@ const createStartContract = (): Start => {
     getEmbeddableFactories: jest.fn(),
     getEmbeddableFactory: jest.fn(),
     EmbeddablePanel: jest.fn(),
-    getAttributeService: jest.fn(),
     getEmbeddablePanel: jest.fn(),
     getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer),
   };
diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx
index 319cbf8ec44b4..3cbd49279564f 100644
--- a/src/plugins/embeddable/public/plugin.tsx
+++ b/src/plugins/embeddable/public/plugin.tsx
@@ -37,10 +37,8 @@ import {
   defaultEmbeddableFactoryProvider,
   IEmbeddable,
   EmbeddablePanel,
-  SavedObjectEmbeddableInput,
 } from './lib';
 import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
-import { AttributeService } from './lib/embeddables/attribute_service';
 import { EmbeddableStateTransfer } from './lib/state_transfer';
 
 export interface EmbeddableSetupDependencies {
@@ -75,14 +73,6 @@ export interface EmbeddableStart {
     embeddableFactoryId: string
   ) => EmbeddableFactory<I, O, E> | undefined;
   getEmbeddableFactories: () => IterableIterator<EmbeddableFactory>;
-  getAttributeService: <
-    A,
-    V extends EmbeddableInput & { attributes: A },
-    R extends SavedObjectEmbeddableInput
-  >(
-    type: string
-  ) => AttributeService<A, V, R>;
-
   EmbeddablePanel: EmbeddablePanelHOC;
   getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC;
   getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer;
@@ -159,7 +149,6 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
     return {
       getEmbeddableFactory: this.getEmbeddableFactory,
       getEmbeddableFactories: this.getEmbeddableFactories,
-      getAttributeService: (type: string) => new AttributeService(type, core.savedObjects.client),
       getStateTransfer: (history?: ScopedHistory) => {
         return history
           ? new EmbeddableStateTransfer(core.application.navigateToApp, history)