diff --git a/modules/chatbot_api_entities/chatbot_api_entities.info.yml b/modules/chatbot_api_entities/chatbot_api_entities.info.yml
new file mode 100644
index 0000000..6fb1e65
--- /dev/null
+++ b/modules/chatbot_api_entities/chatbot_api_entities.info.yml
@@ -0,0 +1,6 @@
+name: Chatbot API Entities
+description: Provides API for pushing Drupal entities to chatbot services
+type: module
+core: 8.x
+dependencies:
+  - chatbot_api
diff --git a/modules/chatbot_api_entities/chatbot_api_entities.links.action.yml b/modules/chatbot_api_entities/chatbot_api_entities.links.action.yml
new file mode 100644
index 0000000..64c50d7
--- /dev/null
+++ b/modules/chatbot_api_entities/chatbot_api_entities.links.action.yml
@@ -0,0 +1,6 @@
+entity.chatbot_api_entities_collection.add_form:
+  route_name: entity.chatbot_api_entities_collection.add_form
+  title: 'Add collection'
+  appears_on:
+    - entity.chatbot_api_entities_collection.collection
+
diff --git a/modules/chatbot_api_entities/chatbot_api_entities.links.menu.yml b/modules/chatbot_api_entities/chatbot_api_entities.links.menu.yml
new file mode 100644
index 0000000..7ed69f9
--- /dev/null
+++ b/modules/chatbot_api_entities/chatbot_api_entities.links.menu.yml
@@ -0,0 +1,7 @@
+entity.chatbot_api_entities_collection.collection:
+  title: 'Entity collection'
+  route_name: entity.chatbot_api_entities_collection.collection
+  description: 'Chatbot API entity collections'
+  parent: system.admin_config_services
+  weight: 99
+
diff --git a/modules/chatbot_api_entities/chatbot_api_entities.module b/modules/chatbot_api_entities/chatbot_api_entities.module
new file mode 100644
index 0000000..d064377
--- /dev/null
+++ b/modules/chatbot_api_entities/chatbot_api_entities.module
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains main module functions.
+ */
+
+use Drupal\chatbot_api_entities\EntityHookDispatcher;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function chatbot_api_entities_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    // Main module help for the helloworld_intent module.
+    case 'help.page.chatbot_api_entities':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('Chatbot API - entities provides a framework for pushing collections of entities to Chatbot services.') . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_entity_delete().
+ */
+function chatbot_api_entities_entity_delete(EntityInterface $entity) {
+  \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityHookDispatcher::class)->handleEntityEvent($entity, 'delete');
+}
+
+/**
+ * Implements hook_entity_insert().
+ */
+function chatbot_api_entities_entity_insert(EntityInterface $entity) {
+  \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityHookDispatcher::class)->handleEntityEvent($entity);
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function chatbot_api_entities_entity_update(EntityInterface $entity) {
+  \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityHookDispatcher::class)->handleEntityEvent($entity, 'update');
+}
diff --git a/modules/chatbot_api_entities/chatbot_api_entities.services.yml b/modules/chatbot_api_entities/chatbot_api_entities.services.yml
new file mode 100644
index 0000000..82666d6
--- /dev/null
+++ b/modules/chatbot_api_entities/chatbot_api_entities.services.yml
@@ -0,0 +1,7 @@
+services:
+  plugin.manager.chatbot_api_entities_query_handler:
+    class: Drupal\chatbot_api_entities\Plugin\QueryHandlerManager
+    parent: default_plugin_manager
+  plugin.manager.chatbot_api_entities_push_handler:
+    class: Drupal\chatbot_api_entities\Plugin\PushHandlerManager
+    parent: default_plugin_manager
diff --git a/modules/chatbot_api_entities/config/schema/chatbot_api_entities_collection.schema.yml b/modules/chatbot_api_entities/config/schema/chatbot_api_entities_collection.schema.yml
new file mode 100644
index 0000000..a5de2f7
--- /dev/null
+++ b/modules/chatbot_api_entities/config/schema/chatbot_api_entities_collection.schema.yml
@@ -0,0 +1,71 @@
+chatbot_api_entities.chatbot_api_entities_collection.*:
+  type: config_entity
+  label: 'Entity collection'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    label:
+      type: label
+      label: 'Label'
+    uuid:
+      type: string
+    entity_type:
+      type: string
+      label: 'Entity Type ID'
+    bundle:
+      type: string
+      label: 'Bundle'
+    synonyms:
+      type: string
+      label: 'Synonyms'
+    query_handlers:
+      type: sequence
+      label: 'Enabled query handlers'
+      sequence:
+        type: chatbot_api_entities_query_handler
+    push_handlers:
+      type: sequence
+      label: 'Enabled push handlers'
+      sequence:
+        type: chatbot_api_entities_push_handler
+
+chatbot_api_entities_push_handler:
+  type: mapping
+  label: 'Push handler'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    provider:
+      type: string
+      label: 'Provider'
+    settings:
+      type: chatbot_api_entities_push_handler_settings.[%parent.id]
+
+chatbot_api_entities_push_handler_settings.*:
+  type: sequence
+  label: 'Push settings'
+  sequence:
+    type: string
+    label: 'Value'
+
+chatbot_api_entities_query_handler:
+  type: mapping
+  label: 'Query handler'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    provider:
+      type: string
+      label: 'Provider'
+    settings:
+      type: chatbot_api_entities_query_handler_settings.[%parent.id]
+
+chatbot_api_entities_push_handler_settings.*:
+  type: sequence
+  label: 'Query settings'
+  sequence:
+    type: string
+    label: 'Value'
diff --git a/modules/chatbot_api_entities/src/Annotation/PushHandler.php b/modules/chatbot_api_entities/src/Annotation/PushHandler.php
new file mode 100644
index 0000000..078c24e
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Annotation/PushHandler.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Push handler item annotation object.
+ *
+ * @see \Drupal\chatbot_api_entities\Plugin\PushHandlerManager
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class PushHandler extends Plugin {
+
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The label of the plugin.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $label;
+
+}
diff --git a/modules/chatbot_api_entities/src/Annotation/QueryHandler.php b/modules/chatbot_api_entities/src/Annotation/QueryHandler.php
new file mode 100644
index 0000000..2f354c6
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Annotation/QueryHandler.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Query handler item annotation object.
+ *
+ * @see \Drupal\chatbot_api_entities\Plugin\QueryHandlerManager
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class QueryHandler extends Plugin {
+
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The label of the plugin.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $label;
+
+}
diff --git a/modules/chatbot_api_entities/src/Entity/EntityCollection.php b/modules/chatbot_api_entities/src/Entity/EntityCollection.php
new file mode 100644
index 0000000..fdb15f0
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Entity/EntityCollection.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
+use Drupal\Core\Plugin\DefaultLazyPluginCollection;
+
+/**
+ * Defines the Entity collection entity.
+ *
+ * @ConfigEntityType(
+ *   id = "chatbot_api_entities_collection",
+ *   label = @Translation("Entity collection"),
+ *   handlers = {
+ *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
+ *     "list_builder" = "\Drupal\Core\Entity\EntityListBuilder",
+ *     "form" = {
+ *       "add" = "Drupal\chatbot_api_entities\Form\EntityCollectionForm",
+ *       "edit" = "Drupal\chatbot_api_entities\Form\EntityCollectionForm",
+ *       "delete" =
+ *   "Drupal\chatbot_api_entities\Form\EntityCollectionDeleteForm"
+ *     },
+ *     "route_provider" = {
+ *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
+ *     },
+ *   },
+ *   config_prefix = "chatbot_api_entities_collection",
+ *   admin_permission = "administer site configuration",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label",
+ *     "uuid" = "uuid"
+ *   },
+ *   links = {
+ *     "canonical" =
+ *   "/admin/config/service/chatbot_api_entities_collection/{chatbot_api_entities_collection}",
+ *     "add-form" =
+ *   "/admin/config/service/chatbot_api_entities_collection/add",
+ *     "edit-form" =
+ *   "/admin/config/service/chatbot_api_entities_collection/{chatbot_api_entities_collection}/edit",
+ *     "delete-form" =
+ *   "/admin/config/service/chatbot_api_entities_collection/{chatbot_api_entities_collection}/delete",
+ *     "collection" = "/admin/config/service/chatbot_api_entities_collection"
+ *   },
+ *   config_export = {
+ *     "label",
+ *     "id",
+ *     "synonyms",
+ *     "query_handlers",
+ *     "push_handlers",
+ *     "entity_type",
+ *     "bundle"
+ *   }
+ * )
+ */
+class EntityCollection extends ConfigEntityBase implements EntityCollectionInterface, EntityWithPluginCollectionInterface {
+
+  /**
+   * The Entity collection ID.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The Entity collection label.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * Field name for deriving synonyms.
+   *
+   * @var string
+   */
+  protected $synonyms;
+
+  /**
+   * Bundle for this collection.
+   *
+   * @var string
+   */
+  protected $bundle;
+
+  /**
+   * Entity type ID for this collection.
+   *
+   * @var string
+   */
+  protected $entity_type;
+
+  /**
+   * Query handler configurations.
+   *
+   * @var []
+   */
+  protected $query_handlers;
+
+  /**
+   * Push handler configurations.
+   *
+   * @var []
+   */
+  protected $push_handlers;
+
+  /**
+   * Query plugin collection.
+   *
+   * @var \Drupal\Core\Plugin\DefaultLazyPluginCollection
+   */
+  protected $queryHandlerCollection;
+
+  /**
+   * Push plugin collection.
+   *
+   * @var \Drupal\Core\Plugin\DefaultLazyPluginCollection
+   */
+  protected $pushHandlerCollection;
+
+  /**
+   * Constructs a new EntityCollection object.
+   *
+   * @param array $values
+   *   Values.
+   * @param string $entity_type
+   *   Entity type.
+   */
+  public function __construct(array $values, $entity_type) {
+    parent::__construct($values, $entity_type);
+    $this->queryHandlerCollection = new DefaultLazyPluginCollection(\Drupal::service('plugin.manager.chatbot_api_entities_query_handler'), $this->query_handlers);
+    $this->pushHandlerCollection = new DefaultLazyPluginCollection(\Drupal::service('plugin.manager.chatbot_api_entities_push_handler'), $this->push_handlers);
+  }
+
+  /**
+   * Gets array of synonyms for the given entity if applicable.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   Entity to get synonyms for.
+   *
+   * @return string[]
+   *   Synonyms.
+   */
+  public function getSynonyms(ContentEntityInterface $entity) {
+    if (!$this->synonyms || !$entity->hasField($this->synonyms) || $entity->get($this->synonyms)->isEmpty()) {
+      return [];
+    }
+    $synonyms = [];
+    /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
+    foreach ($entity->get($this->synonyms) as $field_item) {
+      $synonyms[] = $field_item->get('value')->getValue();
+    }
+    return $synonyms;
+  }
+
+  /**
+   * Generates the collection of entities using the query handlers and pushes.
+   *
+   * Calls each query handler in sequence to build up a list of entities then
+   * passes them to each push handler to send to the remote chatbot endpoints.
+   */
+  public function queryAndPush(EntityTypeManagerInterface $entityTypeManager) {
+    $entities = [];
+    /** @var \Drupal\chatbot_api_entities\Plugin\QueryHandlerInterface $plugin */
+    foreach ($this->queryHandlerCollection as $plugin) {
+      $plugin->query($entityTypeManager, $entities, $this);
+    }
+    if (!$entities) {
+      // Nothing matched.
+      return;
+    }
+    /** @var \Drupal\chatbot_api_entities\Plugin\PushHandlerInterface $plugin */
+    foreach ($this->pushHandlerCollection as $plugin) {
+      $plugin->pushEntities($entities, $this);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginCollections() {
+    return [
+      'query_handlers' => $this->queryHandlerCollection,
+      'push_handlers' => $this->pushHandlerCollection,
+    ];
+  }
+
+  /**
+   * Gets the entity type ID for the entities represented collection.
+   *
+   * @return string
+   *   Entity type ID.
+   */
+  public function getCollectionEntityTypeId() {
+    return $this->entity_type;
+  }
+
+  /**
+   * Gets the bundle for entities represented by the collection.
+   *
+   * @return string
+   *   Bundle.
+   */
+  public function getCollectionBundle() {
+    return $this->bundle;
+  }
+
+}
diff --git a/modules/chatbot_api_entities/src/Entity/EntityCollectionInterface.php b/modules/chatbot_api_entities/src/Entity/EntityCollectionInterface.php
new file mode 100644
index 0000000..131cf1b
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Entity/EntityCollectionInterface.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+
+/**
+ * Provides an interface for defining Entity collection entities.
+ */
+interface EntityCollectionInterface extends ConfigEntityInterface {
+
+  /**
+   * Gets array of synonyms for the given entity if applicable.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   Entity to get synonyms for.
+   *
+   * @return string[]
+   *   Synonyms.
+   */
+  public function getSynonyms(ContentEntityInterface $entity);
+
+  /**
+   * Generates the collection of entities using the query handlers and pushes.
+   *
+   * Calls each query handler in sequence to build up a list of entities then
+   * passes them to each push handler to send to the remote chatbot endpoints.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   Entity type manager.
+   */
+  public function queryAndPush(EntityTypeManagerInterface $entityTypeManager);
+
+  /**
+   * Gets the entity type ID for the entities represented collection.
+   *
+   * @return string
+   *   Entity type ID.
+   */
+  public function getCollectionEntityTypeId();
+
+  /**
+   * Gets the bundle for entities represented by the collection.
+   *
+   * @return string
+   *   Bundle.
+   */
+  public function getCollectionBundle();
+
+}
diff --git a/modules/chatbot_api_entities/src/EntityHookDispatcher.php b/modules/chatbot_api_entities/src/EntityHookDispatcher.php
new file mode 100644
index 0000000..ae7a51f
--- /dev/null
+++ b/modules/chatbot_api_entities/src/EntityHookDispatcher.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\chatbot_api_entities;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Queue\QueueFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class EntityHookDispatcher implements ContainerInjectionInterface {
+
+  /**
+   * Entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Collection storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $collectionStorage;
+
+  /**
+   * Queue factory.
+   *
+   * @var \Drupal\Core\Queue\QueueFactory
+   */
+  protected $queueFactory;
+
+  /**
+   * Constructs a new EntityHookDispatcher object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   Entity type manager.
+   * @param \Drupal\Core\Queue\QueueFactory $queueFactory
+   */
+  public function __construct(EntityTypeManagerInterface $entityTypeManager, QueueFactory $queueFactory) {
+    $this->entityTypeManager = $entityTypeManager;
+    $this->collectionStorage = $entityTypeManager->getStorage('chatbot_api_entities_collection');
+    $this->queueFactory = $queueFactory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('queue')
+    );
+  }
+
+  /**
+   * Handles entity lifecycle events.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   Entity that was created, updated or deleted.
+   * @param string $event_name
+   *   Event that occurred
+   */
+  public function handleEntityEvent(EntityInterface $entity, $event_name = 'insert') {
+    $queue = $this->queueFactory->get('chatbot_api_entities_push');
+    foreach ($this->getRelatedChatbotEntityCollections($entity) as $collection) {
+      $queue->createItem([
+        'collection_id' => $collection->id(),
+        'created' => time(),
+      ]);
+    }
+  }
+
+  /**
+   * Gets related chatbot entity collections.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   Entity to find matching collections for.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   Matching chatbot entity collections.
+   */
+  protected function getRelatedChatbotEntityCollections(EntityInterface $entity) {
+    if (!$entity instanceof ContentEntityInterface) {
+      // We don't do anything for non content entities.
+      return [];
+    }
+    return $this->collectionStorage->loadMultiple($this->collectionStorage->getQuery()
+      ->condition('entity_type', $entity->getEntityTypeId())
+      ->condition('bundle', $entity->bundle())
+      ->execute());
+  }
+
+}
diff --git a/modules/chatbot_api_entities/src/Form/EntityCollectionDeleteForm.php b/modules/chatbot_api_entities/src/Form/EntityCollectionDeleteForm.php
new file mode 100644
index 0000000..6b983f3
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Form/EntityCollectionDeleteForm.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Form;
+
+use Drupal\Core\Entity\EntityConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+/**
+ * Builds the form to delete Entity collection entities.
+ */
+class EntityCollectionDeleteForm extends EntityConfirmFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('entity.chatbot_api_entities_collection.collection');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->entity->delete();
+
+    drupal_set_message(
+      $this->t('Collection deleted: @label.', [
+        '@label' => $this->entity->label(),
+      ])
+    );
+
+    $form_state->setRedirectUrl($this->getCancelUrl());
+  }
+
+}
diff --git a/modules/chatbot_api_entities/src/Form/EntityCollectionForm.php b/modules/chatbot_api_entities/src/Form/EntityCollectionForm.php
new file mode 100644
index 0000000..b1822b4
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Form/EntityCollectionForm.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Class EntityCollectionForm.
+ */
+class EntityCollectionForm extends EntityForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    $chatbot_api_entities_collection = $this->entity;
+    $form['label'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Label'),
+      '#maxlength' => 255,
+      '#default_value' => $chatbot_api_entities_collection->label(),
+      '#description' => $this->t("Label for the Entity collection."),
+      '#required' => TRUE,
+    ];
+
+    $form['id'] = [
+      '#type' => 'machine_name',
+      '#default_value' => $chatbot_api_entities_collection->id(),
+      '#machine_name' => [
+        'exists' => '\Drupal\chatbot_api_entities\Entity\EntityCollection::load',
+      ],
+      '#disabled' => !$chatbot_api_entities_collection->isNew(),
+    ];
+
+    /* You will need additional form elements for your custom properties. */
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $chatbot_api_entities_collection = $this->entity;
+    $status = $chatbot_api_entities_collection->save();
+
+    switch ($status) {
+      case SAVED_NEW:
+        drupal_set_message($this->t('Created the %label collection.', [
+          '%label' => $chatbot_api_entities_collection->label(),
+        ]));
+        break;
+
+      default:
+        drupal_set_message($this->t('Saved the %label collection.', [
+          '%label' => $chatbot_api_entities_collection->label(),
+        ]));
+    }
+    $form_state->setRedirectUrl($chatbot_api_entities_collection->toUrl('collection'));
+  }
+
+}
diff --git a/modules/chatbot_api_entities/src/Plugin/ChatbotApiEntities/QueryHandler/DefaultEntity.php b/modules/chatbot_api_entities/src/Plugin/ChatbotApiEntities/QueryHandler/DefaultEntity.php
new file mode 100644
index 0000000..c3f343c
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Plugin/ChatbotApiEntities/QueryHandler/DefaultEntity.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Plugin\ChatbotApiEntities\QueryHandler;
+
+use Drupal\chatbot_api_entities\Annotation\QueryHandler;
+use Drupal\chatbot_api_entities\Entity\EntityCollectionInterface;
+use Drupal\chatbot_api_entities\Plugin\QueryHandlerBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+
+/**
+ * Defines a query handler that just uses entity query to limit as appropriate.
+ *
+ * @QueryHandler(
+ *   id = "default",
+ *   label = @Translation("Default"),
+ *   deriver = "Drupal\chatbot_api_entities\Plugin\Derivative\DefaultEntityDeriver"
+ * )
+ */
+class DefaultEntity extends QueryHandlerBase {
+
+  /**
+   * Query entities to be pushed.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   Entity type manager.
+   * @param \Drupal\Core\Entity\ContentEntityInterface[] $existing
+   *   Existing entities.
+   *
+   * @return \Drupal\Core\Entity\ContentEntityInterface[]
+   *   Loaded entities.
+   */
+  public function query(EntityTypeManagerInterface $entityTypeManager, array $existing = [], EntityCollectionInterface $collection) {
+    $entity_type_id = $collection->getCollectionEntityTypeId();
+    $entity_type = $entityTypeManager->getDefinition($entity_type_id);
+    $query = $this->getQuery($entityTypeManager, $collection, $entity_type);
+    return $existing + $entityTypeManager->getStorage($entity_type_id)->loadMultiple($query->execute);
+  }
+
+  /**
+   * Check if the query handler applies for the given entity type.
+   *
+   * @param string $entity_type_id
+   *   Entity type ID.
+   *
+   * @return bool
+   *   TRUE if applies.
+   */
+  public function applies($entity_type_id) {
+    return TRUE;
+  }
+
+  /**
+   * Generates the query for execution.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   Entity type manager.
+   * @param \Drupal\chatbot_api_entities\Entity\EntityCollectionInterface $collection
+   *   Collection.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   Entity type.
+   *
+   * @return \Drupal\Core\Entity\Query\QueryInterface
+   *   Query to execute.
+   */
+  protected function getQuery(EntityTypeManagerInterface $entityTypeManager, EntityCollectionInterface $collection, $entity_type) {
+    $query = $entityTypeManager->getStorage($entity_type->id())->getQuery();
+    if ($entity_type->hasKey('bundle')) {
+      $query->condition($entity_type->getKey('bundle'), $collection->getCollectionBundle());
+    }
+    if ($entity_type->hasKey('status')) {
+      $query->condition($entity_type->getKey('status'), TRUE);
+    }
+    return $query;
+  }
+
+}
+
diff --git a/modules/chatbot_api_entities/src/Plugin/ChatbotApiEntities/QueryHandler/User.php b/modules/chatbot_api_entities/src/Plugin/ChatbotApiEntities/QueryHandler/User.php
new file mode 100644
index 0000000..6231d6d
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Plugin/ChatbotApiEntities/QueryHandler/User.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Plugin\ChatbotApiEntities\QueryHandler;
+
+use Drupal\chatbot_api_entities\Entity\EntityCollectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+
+/**
+ * Defines a query handler that just uses entity query to limit as appropriate.
+ *
+ * @QueryHandler(
+ *   id = "default:user",
+ *   label = @Translation("Default - User")
+ * )
+ */
+class User extends DefaultEntity {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getQuery(EntityTypeManagerInterface $entityTypeManager, EntityCollectionInterface $collection, $entity_type) {
+    $query = parent::getQuery($entityTypeManager, $collection, $entity_type);
+    $query->condition('status', 1);
+    return $query;
+  }
+
+}
diff --git a/modules/chatbot_api_entities/src/Plugin/Derivative/DefaultEntityDeriver.php b/modules/chatbot_api_entities/src/Plugin/Derivative/DefaultEntityDeriver.php
new file mode 100644
index 0000000..cf88148
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Plugin/Derivative/DefaultEntityDeriver.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a plugin deriver for default entity query handler.
+ */
+class DefaultEntityDeriver extends DeriverBase implements ContainerDeriverInterface {
+
+  /**
+   * The entity type manager
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Creates an DefaultEntityDeriver object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entityTypeManager) {
+    $this->entityTypeManager = $entityTypeManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
+      if (!$entity_type->entityClassImplements(ContentEntityInterface::class)) {
+        continue;
+      }
+      $this->derivatives[$entity_type_id] = $base_plugin_definition;
+      $this->derivatives[$entity_type_id]['label'] = t('@entity_type query', ['@entity_type' => $entity_type->getLabel()]);
+      $this->derivatives[$entity_type_id]['base_plugin_label'] = (string) $base_plugin_definition['label'];
+    }
+
+    return parent::getDerivativeDefinitions($base_plugin_definition);
+  }
+
+}
diff --git a/modules/chatbot_api_entities/src/Plugin/PushHandlerBase.php b/modules/chatbot_api_entities/src/Plugin/PushHandlerBase.php
new file mode 100644
index 0000000..5a5f38c
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Plugin/PushHandlerBase.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Plugin;
+
+use Drupal\chatbot_api_entities\Entity\EntityCollection;
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use GuzzleHttp\ClientInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Base class for Push handler plugins.
+ */
+abstract class PushHandlerBase extends PluginBase implements PushHandlerInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * HTTP client.
+   *
+   * @var \GuzzleHttp\ClientInterface
+   */
+  protected $httpClient;
+
+  /**
+   * Constructs a new PushHandlerBase object.
+   *
+   * @param array $configuration
+   *   Configuration.
+   * @param string $plugin_id
+   *   Plugin ID.
+   * @param mixed $plugin_definition
+   *   Plugin definition.
+   * @param \GuzzleHttp\ClientInterface $httpClient
+   *   HTTP client.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ClientInterface $httpClient) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->httpClient = $httpClient;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('http_client')
+    );
+  }
+
+  /**
+   * Format entities for pushing.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
+   *   Array of entities to push.
+   * @param \Drupal\chatbot_api_entities\Entity\EntityCollection $entityCollection
+   *   Configuration object for the collection.
+   *
+   * @return array
+   *   Array of entries each with keys 'value' and 'synonyms'
+   */
+  protected function formatEntries(array $entities, EntityCollection $entityCollection) {
+    $formatted = [];
+    foreach ($entities as $entity) {
+      $formatted[] = [
+        'value' => $entity->label(),
+        'synonyms' => $entityCollection->getSynonyms($entity),
+      ];
+    }
+    return $formatted;
+  }
+
+}
diff --git a/modules/chatbot_api_entities/src/Plugin/PushHandlerInterface.php b/modules/chatbot_api_entities/src/Plugin/PushHandlerInterface.php
new file mode 100644
index 0000000..e2856d3
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Plugin/PushHandlerInterface.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Plugin;
+
+use Drupal\chatbot_api_entities\Entity\EntityCollection;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Defines an interface for Push handler plugins.
+ */
+interface PushHandlerInterface extends PluginInspectionInterface {
+
+  /**
+   * Push Drupal entities to a remote end-point.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
+   *   Array of entities to push.
+   * @param \Drupal\chatbot_api_entities\Entity\EntityCollection $entityCollection
+   *   Configuration object for the collection.
+   *
+   * @return $this
+   */
+  public function pushEntities(array $entities, EntityCollection $entityCollection);
+
+}
diff --git a/modules/chatbot_api_entities/src/Plugin/PushHandlerManager.php b/modules/chatbot_api_entities/src/Plugin/PushHandlerManager.php
new file mode 100644
index 0000000..9ccbfdb
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Plugin/PushHandlerManager.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Plugin;
+
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+
+/**
+ * Provides the Push handler plugin manager.
+ */
+class PushHandlerManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a new PushHandlerManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/ChatbotApiEntities/PushHandler', $namespaces, $module_handler, 'Drupal\chatbot_api_entities\Plugin\PushHandlerInterface', 'Drupal\chatbot_api_entities\Annotation\PushHandler');
+
+    $this->alterInfo('chatbot_api_entities_chatbot_api_entities_push_handler_info');
+    $this->setCacheBackend($cache_backend, 'chatbot_api_entities_chatbot_api_entities_push_handler_plugins');
+  }
+
+}
diff --git a/modules/chatbot_api_entities/src/Plugin/QueryHandlerBase.php b/modules/chatbot_api_entities/src/Plugin/QueryHandlerBase.php
new file mode 100644
index 0000000..d3da23a
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Plugin/QueryHandlerBase.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Plugin;
+
+use Drupal\Component\Plugin\PluginBase;
+
+/**
+ * Base class for Query handler plugins.
+ */
+abstract class QueryHandlerBase extends PluginBase implements QueryHandlerInterface {
+
+}
diff --git a/modules/chatbot_api_entities/src/Plugin/QueryHandlerInterface.php b/modules/chatbot_api_entities/src/Plugin/QueryHandlerInterface.php
new file mode 100644
index 0000000..d289e34
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Plugin/QueryHandlerInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Plugin;
+
+use Drupal\chatbot_api_entities\Entity\EntityCollectionInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+
+/**
+ * Defines an interface for Query handler plugins.
+ */
+interface QueryHandlerInterface extends PluginInspectionInterface {
+
+  /**
+   * Query entities to be pushed.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   Entity type manager.
+   * @param \Drupal\Core\Entity\ContentEntityInterface[] $existing
+   *   Existing entities.
+   * @param \Drupal\chatbot_api_entities\Entity\EntityCollectionInterface $collection
+   *   Collection object.
+   *
+   * @return \Drupal\Core\Entity\ContentEntityInterface[]
+   *   Loaded entities.
+   */
+  public function query(EntityTypeManagerInterface $entityTypeManager, array $existing = [], EntityCollectionInterface $collection);
+
+  /**
+   * Check if the query handler applies for the given entity type.
+   *
+   * @param string $entity_type_id
+   *   Entity type ID.
+   *
+   * @return bool
+   *   TRUE if applies.
+   */
+  public function applies($entity_type_id);
+
+}
diff --git a/modules/chatbot_api_entities/src/Plugin/QueryHandlerManager.php b/modules/chatbot_api_entities/src/Plugin/QueryHandlerManager.php
new file mode 100644
index 0000000..7cc7afe
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Plugin/QueryHandlerManager.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Plugin;
+
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+
+/**
+ * Provides the Query handler plugin manager.
+ */
+class QueryHandlerManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a new QueryHandlerManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/ChatbotApiEntities/QueryHandler', $namespaces, $module_handler, 'Drupal\chatbot_api_entities\Plugin\QueryHandlerInterface', 'Drupal\chatbot_api_entities\Annotation\QueryHandler');
+
+    $this->alterInfo('chatbot_api_entities_chatbot_api_entities_query_handler_info');
+    $this->setCacheBackend($cache_backend, 'chatbot_api_entities_chatbot_api_entities_query_handler_plugins');
+  }
+
+}
diff --git a/modules/chatbot_api_entities/src/Plugin/QueueWorker/ChatbotEntityPushWorker.php b/modules/chatbot_api_entities/src/Plugin/QueueWorker/ChatbotEntityPushWorker.php
new file mode 100644
index 0000000..aa8ea9a
--- /dev/null
+++ b/modules/chatbot_api_entities/src/Plugin/QueueWorker/ChatbotEntityPushWorker.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\chatbot_api_entities\Plugin\QueueWorker;
+
+use Drupal\Core\Annotation\QueueWorker;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Queue\QueueWorkerBase;
+use Drupal\Core\State\StateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a queue worker for pushing collections to remote chatbot services.
+ *
+ * @QueueWorker(
+ *   id = "chatbot_api_entities_push",
+ *   title = @Translation("Chatbot API entities push"),
+ *   cron = {"time" = 60}
+ * )
+ */
+class ChatbotEntityPushWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * Entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * State handler.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * Constructs a new ChatbotEntityPushWorker object.
+   *
+   * @param array $configuration
+   *   Configuration.
+   * @param string $plugin_id
+   *   Plugin ID.
+   * @param mixed $plugin_definition
+   *   Plugin definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   Entity type manager.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   State storage.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entityTypeManager, StateInterface $state) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entityTypeManager;
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('state')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processItem($data) {
+    $last = $this->state->get('chatbot_api_entities_last_' . $data['collection_id'], 0);
+    if ($last > $data['created']) {
+      // We've already done this since it was created, so no need to do it
+      // again. This can occur if multiple entity updates result in the same
+      // collection ID being queued more than once.
+      return;
+    }
+    /** @var \Drupal\chatbot_api_entities\Entity\EntityCollectionInterface $collection */
+    if ($collection = $this->entityTypeManager->getStorage('chatbot_api_entities_collection')->load($data['collection_id'])) {
+      $collection->queryAndPush($this->entityTypeManager);
+    }
+  }
+
+}
diff --git a/modules/chatbot_api_entities/tests/modules/chatbot_api_entities_test/chatbot_api_entities_test.info.yml b/modules/chatbot_api_entities/tests/modules/chatbot_api_entities_test/chatbot_api_entities_test.info.yml
new file mode 100644
index 0000000..623776c
--- /dev/null
+++ b/modules/chatbot_api_entities/tests/modules/chatbot_api_entities_test/chatbot_api_entities_test.info.yml
@@ -0,0 +1,7 @@
+name: Chatbot API Entities test
+description: Provides test implementations for chatbot API entities
+type: module
+hidden: TRUE
+core: 8.x
+depdencies:
+  - chatbot_api_entities
diff --git a/modules/chatbot_api_entities/tests/modules/chatbot_api_entities_test/config/schema/chatbot_api_entities_test.schema.yml b/modules/chatbot_api_entities/tests/modules/chatbot_api_entities_test/config/schema/chatbot_api_entities_test.schema.yml
new file mode 100644
index 0000000..a9fa1de
--- /dev/null
+++ b/modules/chatbot_api_entities/tests/modules/chatbot_api_entities_test/config/schema/chatbot_api_entities_test.schema.yml
@@ -0,0 +1,6 @@
+chatbot_api_entities_push_handler_settings.chatbot_api_entities_test:
+  type: chatbot_api_entities_push_handler
+  mapping:
+    remote_id:
+      type: string
+      label: 'Remote ID'
diff --git a/modules/chatbot_api_entities/tests/modules/chatbot_api_entities_test/src/Plugin/ChatbotApiEntities/PushHandler/ChatbotApiEntitiesTestHandler.php b/modules/chatbot_api_entities/tests/modules/chatbot_api_entities_test/src/Plugin/ChatbotApiEntities/PushHandler/ChatbotApiEntitiesTestHandler.php
new file mode 100644
index 0000000..c8f36eb
--- /dev/null
+++ b/modules/chatbot_api_entities/tests/modules/chatbot_api_entities_test/src/Plugin/ChatbotApiEntities/PushHandler/ChatbotApiEntitiesTestHandler.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\chatbot_api_entities_test\Plugin\ChatbotApiEntities\PushHandler;
+
+use Drupal\chatbot_api_entities\Entity\EntityCollection;
+use Drupal\chatbot_api_entities\Plugin\PushHandlerBase;
+use Drupal\Core\State\StateInterface;
+use GuzzleHttp\ClientInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a test push handler for storing pushed entities in state.
+ *
+ * @PushHandler(
+ *   id = "chatbot_api_entities_test",
+ *   label = @Translation("API AI entities endpoint")
+ * )
+ */
+class ChatbotApiEntitiesTestHandler extends PushHandlerBase {
+
+  const STATE_KEY = 'chatbot_api_entities_test';
+
+  /**
+   * State storage.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * Constructs a new ChatbotApiEntitiesTestHandler object.
+   *
+   * @param array $configuration
+   *   Configuration.
+   * @param string $plugin_id
+   *   Plugin ID.
+   * @param mixed $plugin_definition
+   *   Plugin definition.
+   * @param \GuzzleHttp\ClientInterface $httpClient
+   *   HTTP client.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   State storage.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ClientInterface $httpClient, StateInterface $state) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $httpClient);
+    $this->state = $state;
+  }
+
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('http_client'),
+      $container->get('state')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function pushEntities(array $entities, EntityCollection $entityCollection) {
+    $remote_id = $this->configuration['remote_id'];
+    $stored = $this->state->get(self::STATE_KEY, []);
+    $stored[$remote_id] = $this->formatEntries($entities, $entityCollection);
+    $this->state->set(self::STATE_KEY, $stored);
+    return $this;
+  }
+
+}
diff --git a/modules/chatbot_api_entities/tests/src/Kernel/ChatbotApiEntityIntegrationTest.php b/modules/chatbot_api_entities/tests/src/Kernel/ChatbotApiEntityIntegrationTest.php
new file mode 100644
index 0000000..50dce18
--- /dev/null
+++ b/modules/chatbot_api_entities/tests/src/Kernel/ChatbotApiEntityIntegrationTest.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace Drupal\Tests\chatbot_api_entities\Kernel;
+
+use Drupal\chatbot_api_entities_test\Plugin\ChatbotApiEntities\PushHandler\ChatbotApiEntitiesTestHandler;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\chatbot_api_entities\Entity\EntityCollection;
+
+/**
+ * Tests chatbot api integration with entity hooks.
+ *
+ * @group chatbot_api_entities
+ */
+class ChatbotApiEntityIntegrationTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'entity_test',
+    'chatbot_api',
+    'chatbot_api_entities',
+    ChatbotApiEntitiesTestHandler::STATE_KEY,
+    'field',
+    'text',
+    'system',
+    'user',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('user');
+    $this->installSchema('system', ['sequences']);
+    $this->setupEntityTestBundle();
+  }
+
+  /**
+   * Test entity events.
+   */
+  public function testEntityEventsTriggerUpdatesInPlugin() {
+    $config = EntityCollection::create([
+      'id' => 'entity_test_some_bundle',
+      'label' => 'Send entity test some bundle',
+      'entity_type' => 'entity_test',
+      'bundle' => 'some_bundle',
+      'synonyms' => 'field_synonyms',
+      'query_handlers' => [
+        ['id' => 'default:entity_test'],
+      ],
+      'push_handlers' => [
+        [
+          'id' => ChatbotApiEntitiesTestHandler::STATE_KEY,
+          'settings' => [
+            'remote_id' => 'Foobar',
+          ],
+        ],
+      ],
+    ]);
+    $config->save();
+
+    // Create first entity.
+    $entity_test = EntityTest::create([
+      'type' => 'some_bundle',
+      'field_synonyms' => [
+        'bob',
+        'alice',
+        'terry',
+      ],
+      'name' => 'davo',
+    ]);
+    $entity_test->save();
+    $this->cronRun();
+
+    // Assert push handler was called.
+    $sent = $this->container->get('state')
+      ->get(ChatbotApiEntitiesTestHandler::STATE_KEY, []);
+    $this->assertEquals([
+      'Foobar' => [
+        ['value' => 'davo', 'synonyms' => ['bob', 'alice', 'terry']],
+      ],
+    ], $sent);
+
+    // Create another entity.
+    $second_entity = EntityTest::create([
+      'type' => 'some_bundle',
+      'field_synonyms' => [
+        'wayne',
+        'tez',
+        'barry',
+      ],
+      'name' => 'rod',
+    ]);
+    $second_entity->save();
+    $this->cronRun();
+
+    // Assert both have been sent.
+    $sent = $this->container->get('state')
+      ->get(ChatbotApiEntitiesTestHandler::STATE_KEY, []);
+    $this->assertEquals([
+      'Foobar' => [
+        ['value' => 'davo', 'synonyms' => ['bob', 'alice', 'terry']],
+        ['value' => 'rod', 'synonyms' => ['wayne', 'tez', 'barry']],
+      ],
+    ], $sent);
+
+    // Now update the first one.
+    $entity_test->field_synonyms = [
+      'bob',
+      'alice',
+      'terry',
+      'jimmy',
+    ];
+    $entity_test->save();
+    $this->cronRun();
+
+    // Assert updated.
+    $sent = $this->container->get('state')
+      ->get(ChatbotApiEntitiesTestHandler::STATE_KEY, []);
+    $this->assertEquals([
+      'Foobar' => [
+        ['value' => 'davo', 'synonyms' => ['bob', 'alice', 'terry', 'jimmy']],
+        ['value' => 'rod', 'synonyms' => ['wayne', 'tez', 'barry']],
+      ],
+    ], $sent);
+
+    // Now delete.
+    $second_entity->delete();
+    $this->cronRun();
+
+    // Assert updated.
+    $sent = $this->container->get('state')
+      ->get(ChatbotApiEntitiesTestHandler::STATE_KEY, []);
+    $this->assertEquals([
+      'Foobar' => [
+        ['value' => 'davo', 'synonyms' => ['bob', 'alice', 'terry', 'jimmy']],
+      ],
+    ], $sent);
+  }
+
+  /**
+   * Creates a new entity test bundle and adds synonyms field.
+   */
+  protected function setupEntityTestBundle() {
+    $this->installEntitySchema('entity_test');
+    $field_name = 'field_synonyms';
+    $entity_type = 'entity_test';
+    $bundle = 'some_bundle';
+    entity_test_create_bundle($bundle);
+    FieldStorageConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => $entity_type,
+      'type' => 'text',
+      'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+    ])->save();
+
+    $field_config = FieldConfig::create([
+      'field_name' => $field_name,
+      'label' => $field_name,
+      'entity_type' => $entity_type,
+      'bundle' => $bundle,
+      'required' => FALSE,
+    ]);
+    $field_config->save();
+  }
+
+  /**
+   * Runs cron.
+   */
+  protected function cronRun() {
+    $this->container->get('cron')->run();
+  }
+
+}
diff --git a/modules/chatbot_api_entities/tests/todo.txt b/modules/chatbot_api_entities/tests/todo.txt
new file mode 100644
index 0000000..40a85ff
--- /dev/null
+++ b/modules/chatbot_api_entities/tests/todo.txt
@@ -0,0 +1,4 @@
+- config dependencies
+- config form
+- api so that save notifies each plugin before saving
+- api ai handler needs to post on new entity so that id can be set
