diff --git a/css/salesforce.css b/css/salesforce.css
new file mode 100644
index 0000000000000000000000000000000000000000..5467576b840043eeece9f83574c514377a38d344
--- /dev/null
+++ b/css/salesforce.css
@@ -0,0 +1,18 @@
+form#salesforce-mapping-fields-form div.even {
+  /* Why doesn't this work? */
+  background-color: lightgray;
+}
+
+form#salesforce-mapping-fields-form div.field_mapping_field > .form-item {
+  float: left;
+  width: 23%;
+  padding-left: 1em;
+}
+
+form#salesforce-mapping-fields-form div.field_mapping_field {
+  clear: left;
+}
+
+form#salesforce-mapping-fields-form #edit-buttons {
+  clear: both;
+}
diff --git a/modules/salesforce_mapping/salesforce_mapping.module b/modules/salesforce_mapping/salesforce_mapping.module
index c72a21e47a95ff0d53e8bc03393253c8ded64464..938c6ddc493b15e3dc7c98da5910543aa74a5a0f 100644
--- a/modules/salesforce_mapping/salesforce_mapping.module
+++ b/modules/salesforce_mapping/salesforce_mapping.module
@@ -100,9 +100,9 @@ function salesforce_mapping_load($name) {
   $mapping = \Drupal::entityTypeManager()
     ->getStorage('salesforce_mapping')
     ->load($name);
-  if (empty($mapping)) {
-    throw new Exception("No mapping found for $name.");
-  }
+  //if (empty($mapping)) {
+  //  throw new Exception("No mapping found for $name.");
+  //}
   return $mapping;
 }
 
@@ -124,9 +124,9 @@ function salesforce_mapping_load_multiple($properties = []) {
   $mappings = \Drupal::entityTypeManager()
     ->getStorage('salesforce_mapping')
     ->loadByProperties($properties);
-  if (empty($mappings)) {
-    throw new Exception('No mappings found.');
-  }
+  //if (empty($mappings)) {
+  //  throw new Exception('No mappings found.');
+  //}
   return $mappings;
 }
 
@@ -157,9 +157,12 @@ function salesforce_mapped_object_load_multiple($properties = []) {
   $mappings = \Drupal::entityTypeManager()
     ->getStorage('salesforce_mapped_object')
     ->loadByProperties($properties);
-  if (empty($mappings)) {
-    throw new Exception('No mapped objects found');
-  }
+  // Do we really want to throw an exception here?
+  // What if we're trying to load an object in a process
+  // that needs to create a new object?
+  //if (empty($mappings)) {
+  //  throw new Exception('No mapped objects found');
+  //}
   return $mappings;
 }
 
@@ -168,7 +171,7 @@ function salesforce_mapped_object_load_multiple($properties = []) {
  */
 function salesforce_mapped_object_load_by_drupal($entity_type, $entity_id) {
   return salesforce_mapped_object_load_multiple([
-    'entity_type_id' => $entity_type, 
+    'entity_type_id' => $entity_type,
     'entity_id' => $entity_id
   ]);
 }
@@ -200,7 +203,10 @@ function salesforce_mapping_get_mapped_objects() {
   $mappings = salesforce_mapping_load_multiple();
   usort($mappings, 'salesforce_mapping_sort');
   foreach ($mappings as $mapping) {
-    $object_types[$mapping->getSalesforceObjectType()] = $mapping->$mapping->getSalesforceObjectType();
+    $type = $mapping->getSalesforceObjectType();
+    // @TODO Why is the assignment $mapping->$mapping?
+    //$object_types[$mapping->getSalesforceObjectType()] = $mapping->$mapping->getSalesforceObjectType();
+    $object_types[$type] = $type;
   }
   return $object_types;
 }
@@ -209,36 +215,10 @@ function salesforce_mapping_get_mapped_objects() {
  * Sort mappings by weight.
  */
 function salesforce_mapping_sort($mapping_a, $mapping_b) {
-  if ($mapping_a->weight == $mapping_b->weight) {
+  if ($mapping_a->get('weight') == $mapping_b->get('weight')) {
     return 0;
   }
-  return ($mapping_a->weight < $mapping_b->weight) ? -1 : 1;
-}
-
-
-/**
- * Implements hook_entity_delete().
- */
-function salesforce_mapping_entity_delete(EntityInterface $entity) {
-  // Avoid recursion. Mapped Objects cannot be mapped themselves.
-  if ($entity instanceof MappedObject) {
-    return;
-  }
-
-  // Delete any Salesforce object mappings with this entity.
-  try {
-    $mapped_objects = salesforce_mapped_object_load_by_entity($entity);
-  }
-  catch (Exception $e) {
-    // No mapped objects, nothing to do.
-    return;
-  }
-
-  // We only deal with mapped objects here. If SF records need to be deleted,
-  // salesforce_push will handle them.
-  foreach ($mapped_objects as $mapped_object) {
-    $mapped_object->delete();
-  }
+  return ($mapping_a->get('weight') < $mapping_b->get('weight')) ? -1 : 1;
 }
 
 /**
diff --git a/modules/salesforce_mapping/src/Entity/MappedObject.php b/modules/salesforce_mapping/src/Entity/MappedObject.php
index d7e594ed90fcbcb3663a64c8c80d61f72211d1c7..0fc3eba258045923b3b0b09a5cebdf2a834f485d 100644
--- a/modules/salesforce_mapping/src/Entity/MappedObject.php
+++ b/modules/salesforce_mapping/src/Entity/MappedObject.php
@@ -7,14 +7,17 @@
 
 namespace Drupal\salesforce_mapping\Entity;
 
-use Drupal\Core\Entity\RevisionableContentEntityBase;
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Entity\EntityChangedTrait;
 use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\Core\Entity\EntityChangedTrait;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\RevisionableContentEntityBase;
+use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\salesforce_mapping\Entity\MappedObjectInterface;
 use Drupal\salesforce\SFID;
+use Drupal\salesforce\SalesforceEvents;
+use Drupal\salesforce_mapping\Entity\MappedObjectInterface;
+use Drupal\salesforce_mapping\PushParams;
+use Drupal\salesforce_mapping\SalesforcePushEvent;
 use Drupal\user\UserInterface;
 
 /**
@@ -80,6 +83,7 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject
    * {@inheritdoc}
    */
   public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $i = 0;
     // We can't use an entity reference, which requires a single entity type. We need to accommodate a reference to any entity type, as specified by entity_type_id
     $fields['entity_id'] = BaseFieldDefinition::create('integer')
       ->setLabel(t('Entity ID'))
@@ -163,7 +167,6 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject
       ->setLabel(t('Changed'))
       ->setDescription(t('The time that the object mapping was last edited.'))
       ->setRevisionable(TRUE)
-      ->setTranslatable(TRUE)
       ->setDisplayOptions('view', [
         'label' => 'above',
         'type' => 'string',
@@ -193,7 +196,7 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject
       ->setSetting('max_length', SALESFORCE_MAPPING_TRIGGER_MAX_LENGTH)
       ->setRevisionable(TRUE);
 
-    // @see ContentEntityBase::baseFieldDefinitions 
+    // @see ContentEntityBase::baseFieldDefinitions
     // and RevisionLogEntityTrait::revisionLogBaseFieldDefinitions
     $fields += parent::baseFieldDefinitions($entity_type);
 
@@ -239,7 +242,12 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject
       ->getStorage($this->entity_type_id->value)
       ->load($this->entity_id->value);
 
-    $params = $mapping->getPushParams($drupal_entity);
+    // previously hook_salesforce_push_params_alter
+    $params = new PushParams($mapping, $drupal_entity);
+    \Drupal::service('event_dispatcher')->dispatch(
+      SalesforceEvents::PUSH_PARAMS,
+      new SalesforcePushEvent($this, $params)
+    );
 
     // @TODO is this the right place for this logic to live?
     // Cases:
@@ -254,7 +262,7 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject
         $mapping->getSalesforceObjectType(),
         $mapping->getKeyField(),
         $mapping->getKeyValue($drupal_entity),
-        $params
+        $params->getParams()
       );
     }
     elseif ($this->sfid()) {
@@ -262,14 +270,14 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject
       $client->objectUpdate(
         $mapping->getSalesforceObjectType(),
         $this->sfid(),
-        $params
+        $params->getParams()
       );
     }
     else {
       $action = 'create';
       $result = $client->objectCreate(
         $mapping->getSalesforceObjectType(),
-        $params
+        $params->getParams()
       );
     }
 
@@ -297,8 +305,7 @@ class MappedObject extends RevisionableContentEntityBase implements MappedObject
       ->set('last_sync_action', 'push_delete')
       ->set('last_sync_status', TRUE)
       ->save();
-
-    return $result;
+    return $this;
   }
 
   public function pull(array $sf_object = NULL, EntityInterface $drupal_entity = NULL) {
diff --git a/modules/salesforce_mapping/src/Entity/SalesforceMapping.php b/modules/salesforce_mapping/src/Entity/SalesforceMapping.php
index 503ce28dadee22d1472cc4fa29900f3855419d1e..a5b7420bae216be849ffd7f2e067cda2642c8280 100644
--- a/modules/salesforce_mapping/src/Entity/SalesforceMapping.php
+++ b/modules/salesforce_mapping/src/Entity/SalesforceMapping.php
@@ -12,6 +12,8 @@ use Drupal\salesforce_mapping\SalesforceMappingFieldPluginManager;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\salesforce_mapping\Entity\SalesforceMappingInterface;
 use Drupal\salesforce\Exception;
+use Drupal\salesforce\SalesforceEvents;
+use Drupal\salesforce_mapping\PushParams;
 
 /**
  * Defines a Salesforce Mapping configuration entity class.
@@ -173,30 +175,6 @@ class SalesforceMapping extends ConfigEntityBase implements SalesforceMappingInt
     return parent::save();
   }
 
-  /**
-   * Given a Drupal entity, return an array of Salesforce key-value pairs
-   *
-   * @param object $entity
-   *   Entity wrapper object.
-   *
-   * @return array
-   *   Associative array of key value pairs.
-   * @see salesforce_push_map_params (from d7)
-   */
-  public function getPushParams(EntityInterface $entity) {
-    // @TODO This should probably be delegated to a field plugin bag?
-    foreach ($this->getFieldMappings() as $field_plugin) {
-      // Skip fields that aren't being pushed to Salesforce.
-      if (!$field_plugin->push()) {
-        continue;
-      }
-      $params[$field_plugin->config('salesforce_field')] = $field_plugin->value($entity);
-    }
-    // @TODO make this an event
-    // drupal_alter('salesforce_push_params', $params, $mapping, $entity_wrapper);
-    return $params;
-  }
-
   /**
    * Given a Salesforce object, return an array of Drupal entity key-value pairs
    *
@@ -264,7 +242,7 @@ class SalesforceMapping extends ConfigEntityBase implements SalesforceMappingInt
   public function getFieldMapping(array $field) {
     return $this->fieldManager->createInstance(
       $field['drupal_field_type'],
-      $field
+      $field['config']
     );
   }
 
@@ -272,64 +250,13 @@ class SalesforceMapping extends ConfigEntityBase implements SalesforceMappingInt
 
   }
 
-  /**
-   * Helper function returns boolean whether this mapping responds to
-   * Drupal CRUD operation(s).
-   *
-   * @return bool
-   */
-  public function doesPush(array $ops = []) {
-    $ops = [
-      SALESFORCE_MAPPING_SYNC_DRUPAL_CREATE,
-      SALESFORCE_MAPPING_SYNC_DRUPAL_UPDATE,
-      SALESFORCE_MAPPING_SYNC_DRUPAL_DELETE,
-    ];
-    return $this->doesCrud($ops);
-  }
-
-  /**
-   * Helper function returns boolean whether this mapping responds to
-   * Salesforce CRUD operation(s).
-   *
-   * @return bool
-   */
-  public function doesPull() {
-    $ops = [
-      SALESFORCE_MAPPING_SYNC_SF_CREATE,
-      SALESFORCE_MAPPING_SYNC_SF_UPDATE,
-      SALESFORCE_MAPPING_SYNC_SF_DELETE,
-    ];
-    return $this->doesCrud($ops);
-  }
-
-  /**
-   * Helper function returns boolean whether this mapping responds to given
-   * Salesforce or Drupal CRUD operation(s).
-   *
-   * @param array $ops (optional)
-   *  Array containing one or more of:
-   *   * SALESFORCE_MAPPING_SYNC_DRUPAL_CREATE
-   *   * SALESFORCE_MAPPING_SYNC_DRUPAL_UPDATE
-   *   * SALESFORCE_MAPPING_SYNC_DRUPAL_DELETE
-   *   * SALESFORCE_MAPPING_SYNC_SF_CREATE
-   *   * SALESFORCE_MAPPING_SYNC_SF_UPDATE
-   *   * SALESFORCE_MAPPING_SYNC_SF_DELETE
-   *
-   *   If empty, treat as if all values were provided.
-   * @return bool
-   */
-  public function doesCrud(array $ops = []) {
-    if (empty($ops)) {
-      $ops = [
-        SALESFORCE_MAPPING_SYNC_DRUPAL_CREATE,
-        SALESFORCE_MAPPING_SYNC_DRUPAL_UPDATE,
-        SALESFORCE_MAPPING_SYNC_DRUPAL_DELETE,
-        SALESFORCE_MAPPING_SYNC_SF_CREATE,
-        SALESFORCE_MAPPING_SYNC_SF_UPDATE,
-        SALESFORCE_MAPPING_SYNC_SF_DELETE
-      ];
+  public function checkTriggers(array $triggers) {
+    foreach ($triggers as $trigger) {
+      if ($this->sync_triggers[$trigger] == 1) {
+        return TRUE;
+      }
     }
-    return !empty(array_intersect($ops, array_keys(array_filter($this->sync_triggers))));
+    return FALSE;
   }
 
 }
diff --git a/modules/salesforce_mapping/src/Form/MappedObjectForm.php b/modules/salesforce_mapping/src/Form/MappedObjectForm.php
index 39feefba4d5ca27280b2326071d3783b3c076337..fd19d41bdce37c8a76b5ccd76c3c05df6f3a005e 100644
--- a/modules/salesforce_mapping/src/Form/MappedObjectForm.php
+++ b/modules/salesforce_mapping/src/Form/MappedObjectForm.php
@@ -17,6 +17,7 @@ use Drupal\Core\Entity\ContentEntityForm;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityStorageControllerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\salesforce\Exception;
 use Drupal\salesforce_mapping\Entity\MappedObject;
 use Drupal\salesforce_mapping\Entity\SalesforceMapping;
 use Drupal\salesforce_mapping\SalesforceMappingFieldPluginInterface;
@@ -106,7 +107,13 @@ class MappedObjectForm extends ContentEntityForm {
     }
 
     // Push to SF.
-    $result = $mapped_object->push();
+    try {
+      $result = $mapped_object->push();
+    }
+    catch (Exception $e) {
+      drupal_set_message(t('Push failed with an exception: %exception', array('%exception' => $e->getMessage())), 'error');
+      return;
+    }
     $mapped_object
       ->set('salesforce_id', $result['id'])
       ->save();
@@ -148,7 +155,6 @@ class MappedObjectForm extends ContentEntityForm {
    */
   public function save(array $form, FormStateInterface $form_state) {
     $this->getEntity()->save();
-    // $this->entity->save();
     drupal_set_message($this->t('The mapping has been successfully saved.'));
   }
 
diff --git a/modules/salesforce_mapping/src/Form/SalesforceMappingFieldsForm.php b/modules/salesforce_mapping/src/Form/SalesforceMappingFieldsForm.php
index c1332204b70d56aa017d690fff5209215d1d8e9e..ad433ee677d97257c7c2c637154286560f4b4384 100644
--- a/modules/salesforce_mapping/src/Form/SalesforceMappingFieldsForm.php
+++ b/modules/salesforce_mapping/src/Form/SalesforceMappingFieldsForm.php
@@ -10,12 +10,13 @@ namespace Drupal\salesforce_mapping\Form;
 use Symfony\Component\Debug\Debug;
 
 use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Ajax\CommandInterface;
 use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Ajax\CommandInterface;
 use Drupal\Core\Ajax\InsertCommand;
+use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubformState;
 use Drupal\salesforce_mapping\SalesforceMappingFieldPluginInterface as FieldPluginInterface;
 
 /**
@@ -30,6 +31,7 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
     $form['#entity'] = $this->entity;
+    $form['#attached']['library'][] = 'salesforce/admin';
     // For each field on the map, add a row to our table.
     $form['overview'] = ['#markup' => 'Field mapping overview goes here.'];
 
@@ -60,7 +62,7 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
     }
 
     $form['field_mappings_wrapper'] = [
-      '#title' => t('Field map'),
+      '#title' => t('Mapped Fields'),
       '#type' => 'details',
       '#id' => 'edit-field-mappings-wrapper',
       '#open' => TRUE,
@@ -72,18 +74,10 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
 
     $field_mappings_wrapper['field_mappings'] = [
       '#tree' => TRUE,
-      '#type' => 'table',
+      '#type' => 'container',
       // @TODO there's probably a better way to tie ajax callbacks to this element than by hard-coding an HTML DOM ID here.
       '#id' => 'edit-field-mappings',
-      '#header' => [
-        // @TODO: there must be a better way to get two fields in the same cell than to create an extraneous column
-        'drupal_field_type' => '',
-        'drupal_field_type_label' => $this->t('Field type'),
-        'drupal_field_value' => $this->t('Drupal field'),
-        'salesforce_field' => $this->t('Salesforce field'),
-        'direction' => $this->t('Direction'),
-        'ops' => $this->t('Operations'),
-      ],
+      '#attributes' => ['class' => ['container-striped']],
     ];
     $rows = &$field_mappings_wrapper['field_mappings'];
 
@@ -94,21 +88,8 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
       ],
     ];
 
-    // @TODO figure out how D8 does tokens
-    // $form['field_mappings_wrapper']['token_tree'] = array(
-    //   '#type' => 'container',
-    //   '#attributes' => array(
-    //     'id' => array('edit-token-tree'),
-    //   ),
-    // );
-    // $form['field_mappings_wrapper']['token_tree']['tree'] = array(
-    //   '#theme' => 'token_tree',
-    //   '#token_types' => array($drupal_entity_type),
-    //   '#global_types' => TRUE,
-    // );
     $add_field_text = !empty($field_mappings) ? t('Add another field mapping') : t('Add a field mapping to get started');
 
-
     $form['buttons'] = ['#type' => 'container'];
     $form['buttons']['field_type'] = [
       '#title' => t('Field Type'),
@@ -134,19 +115,29 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
       ],
     ];
 
-    // Field mapping form.
-    $has_token_type = FALSE;
+    $row_template = [
+      '#type' => 'container',
+      '#attributes' => ['class' => ['field_mapping_field', 'row']]
+    ];
 
     // Add a row for each saved mapping
+    $zebra = 0;
     foreach ($this->entity->getFieldMappings() as $field_plugin) {
-      $rows[] = $this->get_row($field_plugin, $form, $form_state);
+      $row = $row_template;
+      $row['#attributes']['class']['zebra'] = ($zebra % 2) ? 'odd' : 'even';
+      $rows[] = $row + $this->get_row($field_plugin, $form, $form_state);
+      $zebra++;
     }
 
     // Apply any changes from form_state to existing fields.
     $input = $form_state->getUserInput();
     if (!empty($input['field_mappings'])) {
       for ($i = count($this->entity->getFieldMappings()); $i < count($input['field_mappings']); $i++) {
-        $rows[] = $this->get_row($this->entity->getFieldMapping($input['field_mappings'][$i]), $form, $form_state);
+        $row = $row_template;
+        $row['#attributes']['class']['zebra'] = ($zebra % 2) ? 'odd' : 'even';
+        $field_plugin = $this->entity->getFieldMapping($input['field_mappings'][$i]);
+        $rows[] = $row + $this->get_row($field_plugin, $form, $form_state);
+        $zebra++;
       }
     }
 
@@ -160,7 +151,10 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
     if (!empty($form_state->getValues())
     && $form_state->getValue('add') == $form_state->getValue('op')
     && !empty($input['field_type'])) {
-      $rows[] = $this->get_row(NULL, $form, $form_state);
+      $row = $row_template;
+      $row['#attributes']['class']['zebra'] = ($zebra % 2) ? 'odd' : 'even';
+      $rows[] = $row + $this->get_row(NULL, $form, $form_state);
+      $zebra++;
     }
 
     // Retrieve and add the form actions array.
@@ -190,9 +184,8 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
    */
   private function get_row(FieldPluginInterface $field_plugin = NULL, $form, FormStateInterface $form_state) {
     $input = $form_state->getUserInput();
-
     if ($field_plugin != NULL) {
-      $field_type = $field_plugin->config('drupal_field_type');
+      $field_type = $field_plugin->getPluginId();
       $field_plugin_definition = $this->get_field_plugin($field_type);
     }
     else {
@@ -205,62 +198,35 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
     }
 
     if (empty($field_type)) {
-      // @TODO throw an exception here ?
-      return;
+      throw new Exception('Invalid field type configuration');
     }
 
     if (empty($field_plugin_definition)) {
-      // @TODO throw an exception here ?
-      return;
+      throw new Exception('No field plugin definition found for ' . $field_type);
     }
 
-    // @TODO allow plugins to override forms for all these fields
-    $row['drupal_field_type'] = [
-        '#type' => 'hidden',
-        '#value' => $field_type,
-    ];
-    $row['drupal_field_type_label'] = [
-        '#markup' => $field_plugin_definition['label'],
-    ];
-
-    // Display the plugin config form here:
-    $row['drupal_field_value'] = $field_plugin->buildConfigurationForm($form, $form_state);
-
-    $row['salesforce_field'] = [
-      '#type' => 'select',
-      '#description' => t('Select a Salesforce field to map.'),
-      '#multiple' => (isset($drupal_field_type['salesforce_multiple_fields']) && $drupal_field_type['salesforce_multiple_fields']) ? TRUE : FALSE,
-      '#options' => $this->get_salesforce_field_options(),
-      '#default_value' => $field_plugin->config('salesforce_field'),
-      '#empty_option' => $this->t('- Select -'),
-    ];
-
-    $row['direction'] = [
-      '#type' => 'radios',
-      '#options' => [
-        SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF => t('Drupal to SF'),
-        SALESFORCE_MAPPING_DIRECTION_SF_DRUPAL => t('SF to Drupal'),
-        SALESFORCE_MAPPING_DIRECTION_SYNC => t('Sync'),
-      ],
-      '#required' => TRUE,
-      '#default_value' => $field_plugin->config('direction') ? $field_plugin->config('direction') : SALESFORCE_MAPPING_DIRECTION_SYNC,
-    ];
+    $row['config'] = $field_plugin->buildConfigurationForm($form, $form_state);
 
     // @TODO implement "lock/unlock" logic here:
     // @TODO convert these to AJAX operations
     $operations = [
-      'locked' => $this->t('Lock'),
+      // 'locked' => $this->t('Lock'),
       'delete' => $this->t('Delete')
     ];
     $defaults = [];
-    if ($field_plugin->config('locked')) {
-      $defaults = ['lock'];
-    }
+    // if ($field_plugin->config('locked')) {
+    //   $defaults = ['lock'];
+    // }
     $row['ops'] = [
+      '#title' => t('Operations'),
       '#type' => 'checkboxes',
       '#options' => $operations,
       '#default_value' => $defaults,
     ];
+    $row['drupal_field_type'] = [
+      '#type' => 'hidden',
+      '#value' => $field_plugin->getPluginId()
+    ];
     return $row;
   }
 
@@ -273,23 +239,34 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
     // indexing while removing delete field mappings.
 
     $values = $form_state->getValues();
+    if (empty($values['field_mappings'])) {
+      // No mappings have been added, no validation to be done.
+      return;
+    }
+
     $key = $values['key'];
     $key_mapped = FALSE;
 
+
     foreach ($values['field_mappings'] as $i => $value) {
-      if ($value['salesforce_field'] == $key) {
-        $key_mapped = TRUE;
-      }
       // If a field was deleted, delete it!
       if (!empty($value['ops']['delete'])) {
         $form_state->unsetValue(["field_mappings", "$i"]);
         continue;
       }
-      $values['field_mappings'][$i]['locked'] = !empty($value['ops']['lock']);
 
-      // Remove UI crud from form state array:
-      $form_state->unsetValue(['field_mappings', $i, 'ops']);
-      $form_state->unsetValue('field_type');
+      // Pass validation to field plugins before performing mapping validation.
+      $field_plugin = $this->entity->getFieldMapping($value);
+      $sub_form_state = SubformState::createForSubform($form['field_mappings_wrapper']['field_mappings'][$i], $form, $form_state);
+      $field_plugin->validateConfigurationForm($form['field_mappings_wrapper']['field_mappings'][$i], $sub_form_state);
+
+      // Send to drupal field plugin for additional validation.
+      if ($field_plugin->config('salesforce_field') == $key) {
+        $key_mapped = TRUE;
+      }
+
+      // @TODO what does "locked" even mean?
+      // $values['field_mappings'][$i]['locked'] = !empty($value['ops']['lock']);
     }
 
     if (!empty($key) && !$key_mapped) {
@@ -299,6 +276,23 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
 
   }
 
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Need to transform the schema slightly to remove the "config" dereference. Also trigger submit handlers on plugins.
+    $form_state->unsetValue(['field_type', 'ops']);
+
+    $values = &$form_state->getValues();
+    foreach ($values['field_mappings'] as $i => &$value) {
+      // Pass submit values to plugin submit handler.
+      $field_plugin = $this->entity->getFieldMapping($value);
+      $sub_form_state = SubformState::createForSubform($form['field_mappings_wrapper']['field_mappings'][$i], $form, $form_state);
+      $field_plugin->submitConfigurationForm($form['field_mappings_wrapper']['field_mappings'][$i], $sub_form_state);
+
+      $value = $value + $value['config'];
+      unset($value['config'], $value['ops']);
+    }
+    parent::submitForm($form, $form_state);
+  }
+
   public function field_add_callback($form, FormStateInterface $form_state) {
     $response = new AjaxResponse();
     // Requires updating itself and the field map.
@@ -316,8 +310,6 @@ class SalesforceMappingFieldsForm extends SalesforceMappingFormBase {
   }
 
   protected function get_field_plugin($field_type) {
-    // @TODO not sure if it's best practice to static cache definitions, or just
-    // get them from SalesforceMappingFieldManager each time.
     $field_plugins = $this->SalesforceMappingFieldManager->getDefinitions();
     return $field_plugins[$field_type];
   }
diff --git a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php
index ddb811f2a11d73577ef07834cc53782aaad508db..18fd57edb2b3f274bb8d16769d8c8e4a8bab84ca 100644
--- a/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php
+++ b/modules/salesforce_mapping/src/Form/SalesforceMappingFormCrudBase.php
@@ -181,7 +181,7 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase {
       '#default_value' => $this->entity->get('pull_trigger_date')
         ? $this->entity->get('pull_trigger_date')
         : 'LastModifiedDate',
-      '#options' => $this->get_pull_trigger_options(),
+      '#options' => $this->get_pull_trigger_options($salesforce_object_type),
     ];
 
     // @TODO either change sync_triggers to human readable values, or make them work as hex flags again.
@@ -219,6 +219,10 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase {
    * {@inheritdoc}
    */
   public function validateForm(array &$form, FormStateInterface $form_state) {
+    // fudge the Date Modified form values to get validation to pass on submit
+    if (!empty($form_state->isSubmitted())) {
+      $form['salesforce_object']['pull_trigger_date']['#options'] = $this->get_pull_trigger_options($form_state->getValue('salesforce_object_type'));
+    }
     parent::validateForm($form, $form_state);
 
     $entity_type = $form_state->getValue('drupal_entity_type');
@@ -239,7 +243,7 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase {
       }
     }
   }
- 
+
   /**
    * {@inheritdoc}
    */
@@ -263,6 +267,8 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase {
    */
   public function salesforce_record_type_callback($form, FormStateInterface $form_state) {
     $response = new AjaxResponse();
+    // Set the trigger options based on the selected object
+    $form['salesforce_object']['pull_trigger_date']['#options'] = $this->get_pull_trigger_options($form_state->getValue('salesforce_object_type'));
     // Requires updating itself and the field map.
     $response->addCommand(new ReplaceCommand('#edit-salesforce-object', render($form['salesforce_object'])))->addCommand(new ReplaceCommand('#edit-salesforce-field-mappings-wrapper', render($form['salesforce_field_mappings_wrapper'])));
     return $response;
@@ -293,7 +299,10 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase {
     // arbitrary restriction, but otherwise there would be dozens of entities,
     // making this options list unwieldy.
     foreach ($entity_info as $info) {
-      if (!class_implements($info, 'FieldableEntityInterface')) {
+      if (
+        !in_array('Drupal\Core\Entity\ContentEntityTypeInterface', class_implements($info)) ||
+        $info->id() == 'salesforce_mapped_object'
+      ) {
         continue;
       }
       $options[$info->id()] = $info->getLabel();
@@ -358,7 +367,7 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase {
    * Return form options for available sync triggers.
    *
    * @return array
-   *   Array of sync trigger options keyed by their machine name with their 
+   *   Array of sync trigger options keyed by their machine name with their
    *   label as the value.
    */
   protected function get_sync_trigger_options() {
@@ -376,9 +385,11 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase {
    * Helper function which returns an array of Date fields suitable for use a
    * pull trigger field.
    *
+   * @param string $name
+   *
    * @return array
    */
-  private function get_pull_trigger_options() {
+  private function get_pull_trigger_options($name) {
     $options = [];
     $describe = $this->get_salesforce_object();
     if ($describe) {
@@ -401,5 +412,4 @@ abstract class SalesforceMappingFormCrudBase extends SalesforceMappingFormBase {
     return $field_type_options;
   }
 
-
 }
diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Constant.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Constant.php
index 9f7949270c1421eb9da73f9a09b7d67c46f529d0..25d2b9c0c2595764e2257ec329dbc255a28ec188 100644
--- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Constant.php
+++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Constant.php
@@ -27,11 +27,22 @@ use Drupal\salesforce_mapping\SalesforceMappingFieldPluginBase;
 class Constant extends SalesforceMappingFieldPluginBase {
 
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
-    return [
+    $pluginForm = parent::buildConfigurationForm($form, $form_state);
+
+    $pluginForm['drupal_field_value'] += [
       '#type' => 'textfield',
       '#default_value' => $this->config('drupal_field_value'),
       '#description' => $this->t('Enter a constant value to map to a Salesforce field.'),
     ];
+
+    // @TODO: "Constant" as it's implemented now should only be allowed to be set to "Push". In the future: create "Pull" logic for constant, which pulls a constant value to a Drupal field. Probably a separate mapping field plugin.
+    $pluginForm['direction']['#options'] = [
+      SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF => $pluginForm['direction']['#options'][SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF]
+    ];
+    $pluginForm['direction']['#default_value'] = SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF;
+
+    return $pluginForm;
+
   }
 
   public function value(EntityInterface $entity) {
diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Properties.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Properties.php
index 27a4f4d0ca4c032877505517e94477ff02b03dbc..775a55adb167345d4af99b0c4ae5382c1bcf9861 100644
--- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Properties.php
+++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Properties.php
@@ -28,20 +28,39 @@ class Properties extends SalesforceMappingFieldPluginBase {
    * Implementation of PluginFormInterface::buildConfigurationForm
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $pluginForm = parent::buildConfigurationForm($form, $form_state);
     // @TODO inspecting the form and form_state feels wrong, but haven't found a good way to get the entity from config before the config is saved.
     $options = $this->getConfigurationOptions($form['#entity']);
+
+    // Display the plugin config form here:
     if (empty($options)) {
-      return [
+      $pluginForm['drupal_field_value'] = [
         '#markup' => t('No available properties.')
       ];
     }
-    return [
-      '#type' => 'select',
-      '#options' => $options,
-      '#empty_option' => $this->t('- Select -'),
-      '#default_value' => $this->config('drupal_field_value'),
-      '#description' => $this->t('Select a Drupal field or property to map to a Salesforce field.<br />Entity Reference fields should be handled using Related Entity Ids or Token field types.'),
-    ];
+    else {
+      $pluginForm['drupal_field_value'] += [
+        '#type' => 'select',
+        '#options' => $options,
+        '#empty_option' => $this->t('- Select -'),
+        '#default_value' => $this->config('drupal_field_value'),
+        '#description' => $this->t('Select a Drupal field or property to map to a Salesforce field.<br />Entity Reference fields should be handled using Related Entity Ids or Token field types.'),
+      ];
+    }
+
+    return $pluginForm;
+  }
+
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    parent::validateConfigurationForm($form, $form_state);
+    $vals = $form_state->getValues();
+    $config = $vals['config'];
+    if (empty($config['salesforce_field'])) {
+      $form_state->setError($form['config']['salesforce_field'], t('Salesforce field is required.'));
+    }
+    if (empty($config['drupal_field_value'])) {
+      $form_state->setError($form['config']['drupal_field_value'], t('Drupal field is required.'));
+    }
   }
 
   public function value(EntityInterface $entity) {
diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php
index 1692f46914b02b94040c4e5337788cd9b5e05ca7..8a62aee847857de61156f40e202bf1ede94d074e 100644
--- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php
+++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedIDs.php
@@ -29,21 +29,27 @@ class RelatedIDs extends SalesforceMappingFieldPluginBase {
    * This is basically the inverse of Properties::buildConfigurationForm()
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $pluginForm = parent::buildConfigurationForm($form, $form_state);
+
     // @TODO inspecting the form and form_state feels wrong, but haven't found a good way to get the entity from config before the config is saved.
     $options = $this->getConfigurationOptions($form['#entity']);
 
     if (empty($options)) {
-      return [
+      $pluginForm['drupal_field_value'] += [
         '#markup' => t('No available entity reference fields.')
       ];
     }
-    return [
-      '#type' => 'select',
-      '#options' => $options,
-      '#empty_option' => $this->t('- Select -'),
-      '#default_value' => $this->config('drupal_field_value'),
-      '#description' => $this->t('If an existing connection is found with the selected entity reference, the linked identifier will be used.<br />For example, Salesforce ID for Drupal to SF, or Node ID for SF to Drupal.<br />If more than one entity is referenced, the entity at delta zero will be used.'),
-    ];
+    else {
+      $pluginForm['drupal_field_value'] += [
+        '#type' => 'select',
+        '#options' => $options,
+        '#empty_option' => $this->t('- Select -'),
+        '#default_value' => $this->config('drupal_field_value'),
+        '#description' => $this->t('If an existing connection is found with the selected entity reference, the linked identifier will be used.<br />For example, Salesforce ID for Drupal to SF, or Node ID for SF to Drupal.<br />If more than one entity is referenced, the entity at delta zero will be used.'),
+      ];
+    }
+    return $pluginForm;
+
   }
 
   /**
diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedProperties.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedProperties.php
index a0db2fc8533484f36111be2a9cb66963a99a6a40..94b442afe5eb513c4d8d705e391c9363e472b74c 100644
--- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedProperties.php
+++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/RelatedProperties.php
@@ -29,20 +29,28 @@ class RelatedProperties extends SalesforceMappingFieldPluginBase {
    * This is basically the inverse of Properties::buildConfigurationForm()
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $pluginForm = parent::buildConfigurationForm($form, $form_state);
+
     // @TODO inspecting the form and form_state feels wrong, but haven't found a good way to get the entity from config before the config is saved.
     $options = $this->getConfigurationOptions($form['#entity']);
+
     if (empty($options)) {
-      return [
+      $pluginForm['drupal_field_value'] += [
         '#markup' => t('No available entity reference fields.')
       ];
     }
-    return [
-      '#type' => 'select',
-      '#options' => $options,
-      '#empty_option' => $this->t('- Select -'),
-      '#default_value' => $this->config('drupal_field_value'),
-      '#description' => $this->t('Select a property from the referenced field.<br />If more than one entity is referenced, the entity at delta zero will be used.<br />An entity reference field will be used to sync an identifier, e.g. Salesforce ID and Node ID.'),
-    ];
+    else {
+      $pluginForm['drupal_field_value'] += [
+        '#type' => 'select',
+        '#options' => $options,
+        '#empty_option' => $this->t('- Select -'),
+        '#default_value' => $this->config('drupal_field_value'),
+        '#description' => $this->t('Select a property from the referenced field.<br />If more than one entity is referenced, the entity at delta zero will be used.<br />An entity reference field will be used to sync an identifier, e.g. Salesforce ID and Node ID.'),
+      ];
+    }
+    return $pluginForm;
+
+
   }
 
   public function value(EntityInterface $entity) {
diff --git a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Token.php b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Token.php
index 46b70340f5a5536db659df4ecf2ba24b8f82a176..2ac0279f3fffc4ed2eca4de7eaac90cfcda533db 100644
--- a/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Token.php
+++ b/modules/salesforce_mapping/src/Plugin/SalesforceMappingField/Token.php
@@ -45,14 +45,24 @@ class Token extends SalesforceMappingFieldPluginBase {
   }
 
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $pluginForm = parent::buildConfigurationForm($form, $form_state);
+
     // @TODO expose token options on mapping form: clear, callback, sanitize
     // @TODO expose token tree / selector
     // @TODO add token validation
-    return [
+    $pluginForm['drupal_field_value'] += [
       '#type' => 'textfield',
       '#default_value' => $this->config('drupal_field_value'),
       '#description' => $this->t('Enter a token to map a Salesforce field..'),
     ];
+
+    // @TODO: "Constant" as it's implemented now should only be allowed to be set to "Push". In the future: create "Pull" logic for constant, which pulls a constant value to a Drupal field. Probably a separate mapping field plugin.
+    $pluginForm['direction']['#options'] = [
+      SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF => $pluginForm['direction']['#options'][SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF]
+    ];
+    $pluginForm['direction']['#default_value'] = SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF;
+
+    return $pluginForm;
   }
 
   public function value(EntityInterface $entity) {
diff --git a/modules/salesforce_mapping/src/PushParams.php b/modules/salesforce_mapping/src/PushParams.php
new file mode 100644
index 0000000000000000000000000000000000000000..e95550ab70057f16620a13c718613638d692a928
--- /dev/null
+++ b/modules/salesforce_mapping/src/PushParams.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Drupal\salesforce_mapping;
+
+use Symfony\Component\EventDispatcher\Event;
+use Drupal\salesforce_mapping\Entity\SalesforceMappingInterface;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Wrapper for the array of values which will be pushed to Salesforce.
+ * Usable by salesforce.client for push actions: create, upsert, update
+ */
+class PushParams {
+
+  protected $params;
+  protected $mapping;
+  protected $drupal_entity;
+
+  /**
+   * Given a Drupal entity, return an array of Salesforce key-value pairs
+   * previously salesforce_push_map_params (d7)
+   *
+   * @param SalesforceMappingInterface $mapping
+   * @param EntityInterface $entity
+   * @param array $params (optional)
+   */
+  public function __construct(SalesforceMappingInterface $mapping, EntityInterface $entity, array $params = []) {
+    $this->mapping = $mapping;
+    $this->drupal_entity = $entity;
+    $this->params = $params;
+    foreach ($mapping->getFieldMappings() as $field_plugin) {
+      // Skip fields that aren't being pushed to Salesforce.
+      if (!$field_plugin->push()) {
+        continue;
+      }
+      $this->params[$field_plugin->config('salesforce_field')] =  $field_plugin->value($entity);
+    }
+  }
+
+  public function getMapping() {
+    return $this->mapping;
+  }
+
+  public function getDrupalEntity() {
+    return $this->drupal_entity;
+  }
+
+  public function getParams() {
+    return $this->params;
+  }
+
+  /**
+   * @throws Exception
+   */
+  public function getParam($key) {
+    if (!array_key_exists($key, $this->params)) {
+      throw new Exception("Param key $key does not exist");
+    }
+    return $this->params[$key];
+  }
+
+  public function setParams(array $params) {
+    $this->params = $params;
+    return $this;
+  }
+
+  public function setParam($key, $value) {
+    $this->params[$key] = $value;
+    return $this;
+  }
+
+}
diff --git a/modules/salesforce_mapping/src/SalesforceCrudEvent.php b/modules/salesforce_mapping/src/SalesforceCrudEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..fda18e5201b5c222db8f24d412570b380381635d
--- /dev/null
+++ b/modules/salesforce_mapping/src/SalesforceCrudEvent.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\salesforce_mapping;
+
+use Symfony\Component\EventDispatcher\Event;
+use Drupal\salesforce_mapping\Entity;
+
+class SalesforceCrudEvent extends Event {
+
+  protected $params;
+  protected $mapping;
+  protected $mapped_object;
+  protected $entity;
+
+  public function __construct(EntityInterface $entity, $operation, SalesforceMappingInterface $mapping = NULL, MappedObjectInterface $mapped_object = NULL, PushParams $params = NULL) {
+    $this->entity = $entity;
+    $this->operation = $operation;
+    $this->mapping = $mapping;
+    $this->mapped_object = $mapped_object;
+    $this->params = $params;
+  }
+
+  public function getOperation() {
+    return $this->operation;
+  }
+
+  public function getEntity() {
+    return $this->entity;
+  }
+
+  public function getMapping() {
+    return $this->mapping;
+  }
+
+  public function getMappedObject() {
+    return $this->mapped_object;
+  }
+
+  public function getParams() {
+    return $this->params;
+  }
+
+}
diff --git a/modules/salesforce_mapping/src/SalesforceMappingFieldPluginBase.php b/modules/salesforce_mapping/src/SalesforceMappingFieldPluginBase.php
index c9418eab99af780e7e5d494b8d9bb142ea886bbc..b3ef32cc2ef2efcac6e6519fae6fb0d840d9c423 100644
--- a/modules/salesforce_mapping/src/SalesforceMappingFieldPluginBase.php
+++ b/modules/salesforce_mapping/src/SalesforceMappingFieldPluginBase.php
@@ -103,13 +103,52 @@ abstract class SalesforceMappingFieldPluginBase extends PluginBase implements Sa
     return [
       'direction' => SALESFORCE_MAPPING_DIRECTION_SYNC,
       'salesforce_field' => [],
-      'drupal_field_type' => $this->id,
+      'drupal_field_type' => $this->getPluginId(),
       'drupal_field_value' => '',
       'locked' => FALSE,
       'mapping_name' => '',
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $pluginForm = array();
+    $plugin_def = $this->getPluginDefinition();
+
+    // Extending plugins will probably inject most of their own logic here:
+    $pluginForm['drupal_field_value'] = [
+      '#title' => $plugin_def['label'],
+    ];
+
+    $pluginForm['salesforce_field'] = [
+      '#title' => t('Salesforce field'),
+      '#type' => 'select',
+      '#description' => t('Select a Salesforce field to map.'),
+      // @TODO MULTIPLE SF FIELDS FOR ONE MAPPING FIELD NOT IN USE:
+      // '#multiple' => (isset($drupal_field_type['salesforce_multiple_fields']) && $drupal_field_type['salesforce_multiple_fields']) ? TRUE : FALSE,
+      '#options' => $this->get_salesforce_field_options($form['#entity']->getSalesforceObjectType()),
+      '#default_value' => $this->config('salesforce_field'),
+      '#empty_option' => $this->t('- Select -'),
+    ];
+
+    $pluginForm['direction'] = [
+      '#title' => t('Direction'),
+      '#type' => 'radios',
+      '#options' => [
+        SALESFORCE_MAPPING_DIRECTION_DRUPAL_SF => t('Drupal to SF'),
+        SALESFORCE_MAPPING_DIRECTION_SF_DRUPAL => t('SF to Drupal'),
+        SALESFORCE_MAPPING_DIRECTION_SYNC => t('Sync'),
+      ],
+      '#required' => TRUE,
+      '#default_value' => $this->config('direction') ? $this->config('direction') : SALESFORCE_MAPPING_DIRECTION_SYNC,
+    ];
+
+    return $pluginForm;
+  }
+
+
   /**
    * Implements PluginFormInterface::validateConfigurationForm().
    */
@@ -191,4 +230,32 @@ abstract class SalesforceMappingFieldPluginBase extends PluginBase implements Sa
     return in_array($this->config('direction'), [SALESFORCE_MAPPING_DIRECTION_SYNC, SALESFORCE_MAPPING_DIRECTION_SF_DRUPAL]);
   }
 
+  /**
+   * Helper to retreive a list of fields for a given object type.
+   *
+   * @param string $salesforce_object_type
+   *   The object type of whose fields you want to retreive.
+   *
+   * @return array
+   *   An array of values keyed by machine name of the field with the label as
+   *   the value, formatted to be appropriate as a value for #options.
+   */
+  protected function get_salesforce_field_options($sfobject_name) {
+    static $options;
+    if (!empty($options[$sfobject_name])) {
+      return $options[$sfobject_name];
+    }
+    $sfapi = salesforce_get_api();
+    $sfobject = $sfapi->objectDescribe($sfobject_name);
+    $sf_fields = [];
+    if (isset($sfobject['fields'])) {
+      foreach ($sfobject['fields'] as $sf_field) {
+        $sf_fields[$sf_field['name']] = $sf_field['label'];
+      }
+    }
+    asort($sf_fields);
+    $options[$sfobject_name] = $sf_fields;
+    return $sf_fields;
+  }
+
 }
diff --git a/modules/salesforce_mapping/src/SalesforcePushEvent.php b/modules/salesforce_mapping/src/SalesforcePushEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..57a8888a66f2ffde2efb0e4361efd12f45654550
--- /dev/null
+++ b/modules/salesforce_mapping/src/SalesforcePushEvent.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\salesforce_mapping;
+
+use Symfony\Component\EventDispatcher\Event;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\salesforce_mapping\Entity\SalesforceMappingInterface;
+use Drupal\salesforce_mapping\Entity\MappedObjectInterface;
+
+class SalesforcePushEvent extends Event {
+
+  protected $params;
+  protected $mapping;
+  protected $mapped_object;
+  protected $entity;
+
+  public function __construct(MappedObjectInterface $mapped_object = NULL, PushParams $params = NULL) {
+    $this->mapped_object = $mapped_object;
+    $this->params = $params;
+    $this->entity = $params->getDrupalEntity();
+    $this->mapping = $params->getMapping();
+  }
+
+  public function getEntity() {
+    return $this->entity;
+  }
+
+  public function getMapping() {
+    return $this->mapping;
+  }
+
+  public function getMappedObject() {
+    return $this->mapped_object;
+  }
+
+  public function getParams() {
+    return $this->params;
+  }
+
+}
diff --git a/modules/salesforce_pull/salesforce_pull.module b/modules/salesforce_pull/salesforce_pull.module
index 6cb7ad890f43ae4d67733280b6870d4cfc7f56cc..5af230f3fd1787e21d9e44a215da4200bca4fdfa 100644
--- a/modules/salesforce_pull/salesforce_pull.module
+++ b/modules/salesforce_pull/salesforce_pull.module
@@ -19,22 +19,6 @@ function salesforce_pull_cron() {
   }
 }
 
-/**
- * Implements hook_queue_info().
- */
- /*
-function salesforce_pull_queue_info() {
-  $queues['salesforce_pull'] = array(
-    'title' => t('Salesforce Pull Queue'),
-    'worker callback' => 'salesforce_pull_process_records',
-    // Set to a high max timeout in case pulling in lots of data from SF.
-    'cron' => array(
-      'time' => 180,
-    ),
-  );
-  return $queues;
-}
-*/
 /**
  * Pull updated records from Salesforce and place them in the queue.
  *
@@ -43,7 +27,7 @@ function salesforce_pull_queue_info() {
  */
 function salesforce_pull_get_updated_records(RestClient $sfapi) {
   // @TODO: Refactor all this.
-  return;
+  //return;
   $queue = \Drupal::queue('cron_salesforce_pull');
 
   // Avoid overloading the processing queue and pass this time around if it's
@@ -59,7 +43,7 @@ function salesforce_pull_get_updated_records(RestClient $sfapi) {
 
     // Iterate over each field mapping to determine our query parameters.
     foreach (salesforce_mapping_load_multiple(['salesforce_object_type' => $type]) as $mapping) {
-      foreach ($mapping->field_mappings as $field_map) {
+      foreach ($mapping->get('field_mappings') as $field_map) {
         // Exclude field mappings that are only drupal to SF.
         if (in_array($field_map['direction'], [SALESFORCE_MAPPING_DIRECTION_SYNC, SALESFORCE_MAPPING_DIRECTION_SF_DRUPAL])) {
           // Some field map types (Relation) store a collection of SF objects.
@@ -151,24 +135,19 @@ function salesforce_pull_get_updated_records(RestClient $sfapi) {
   }
 }
 
-/**
- * Process records in the queue.
- */
-function salesforce_pull_process_records($sf_object) {
-  // Moved to QueueWorker plugin class PullBase
-}
-
 /**
  * Process deleted records from salesforce.
  */
 function salesforce_pull_process_deleted_records(RestClient $sfapi) {
+  // @TODO Add back in SOAP, and use autoloading techniques
+  /*
   if (!\Drupal::moduleHandler()->moduleExists('salesforce_soap')) {
     salesforce_set_message('Enable Salesforce SOAP to process deleted records');
     return;
   }
   module_load_include('inc', 'salesforce_soap');
   $soap = new SalesforceSoapPartner($sfapi);
-
+  */
   foreach (array_reverse(salesforce_mapping_get_mapped_objects()) as $type) {
 
     $last_delete_sync = \Drupal::state()->get('salesforce_pull_delete_last_' . $type, REQUEST_TIME);
@@ -178,7 +157,16 @@ function salesforce_pull_process_deleted_records(RestClient $sfapi) {
     $now = $now > $last_delete_sync + 60 ? $now : $now + 60;
     $last_delete_sync_sf = gmdate('Y-m-d\TH:i:s\Z', $last_delete_sync);
     $now_sf = gmdate('Y-m-d\TH:i:s\Z', $now);
-    $deleted = $soap->getDeleted($type, $last_delete_sync_sf, $now_sf);
+    //$deleted = $soap->getDeleted($type, $last_delete_sync_sf, $now_sf);
+    $deleted = $sfapi->apiCall(
+      "sobjects/$type/deleted/?start=$last_delete_sync_sf&end=$now_sf",
+      [],
+      'GET'
+    );
+    // Cast $deleted as object since REST is returning an array instead of
+    // the object the SOAP client apparantly does
+    $deleted = (object) $deleted;
+
     if (!empty($deleted->deletedRecords)) {
       $sf_mappings = salesforce_mapping_load_multiple(
         ['salesforce_object_type' => $type]
@@ -231,6 +219,8 @@ function salesforce_pull_process_deleted_records(RestClient $sfapi) {
  *   sObject of the Salesforce record.
  * @TODO this should move into SalesforceMapping.php
  */
+ /*
+  * Moved to queueworker
 function salesforce_pull_map_fields($field_maps, &$entity_wrapper, $sf_object) {
   foreach ($field_maps as $field_map) {
     if ($field_map['direction'] == 'sync' || $field_map['direction'] == 'sf_drupal') {
@@ -264,6 +254,7 @@ function salesforce_pull_map_fields($field_maps, &$entity_wrapper, $sf_object) {
     }
   }
 }
+ */
 
 /**
  * Implements hook_salesforce_push_entity_allowed()
diff --git a/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php b/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php
index 1c9529682d06fd72f9a1453f337e284dc8702ff7..dc6f8b3c925166a31b9eb7ab41a5b534865c953a 100644
--- a/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php
+++ b/modules/salesforce_pull/src/Plugin/QueueWorker/PullBase.php
@@ -7,11 +7,13 @@
 
 namespace Drupal\salesforce_pull\Plugin\QueueWorker;
 
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Queue\QueueWorkerBase;
 use Drupal\node\NodeInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\salesforce_mapping\Entity\SalesforceMapping;
 
 /**
  * Provides base functionality for the Salesforce Pull Queue Workers.
@@ -43,12 +45,25 @@ abstract class PullBase extends QueueWorkerBase {
       $mapping_conditions['salesforce_record_type'] = $sf_object['RecordTypeId'];
     }
 
-    $sf_mappings = salesforce_mapping_load_multiple($mapping_conditions);
+    try {
+      $sf_mappings = salesforce_mapping_load_multiple($mapping_conditions);
+    }
+    catch (Exception $e) {
+      return;
+    }
 
     foreach ($sf_mappings as $sf_mapping) {
       // Mapping object exists?
-      $mapped_object = salesforce_mapped_object_load_by_sfid($sf_object['Id']);
-      if ($mapped_object && in_array(SALESFORCE_MAPPING_SYNC_SF_UPDATE, $sf_mapping->sync_triggers)) {
+      // @TODO: alex to make this work more better =]
+      try {
+        $mapped_object = salesforce_mapped_object_load_by_sfid($sf_object['Id']);
+        $this->doUpdate($sf_mapping, $mapped_object);
+      }
+      catch (Exception $e) {
+        $this->doCreate();
+      }
+
+      if (!empty($mapped_object) && $sf_mapping->checkTriggers([SALESFORCE_MAPPING_SYNC_SF_UPDATE])) {
         try {
           $entity = \Drupal::entityTypeManager()
             ->getStorage($mapped_object->entity_type_id->value)
@@ -90,40 +105,43 @@ abstract class PullBase extends QueueWorkerBase {
         }
       }
       else {
-        if (in_array(SALESFORCE_MAPPING_SYNC_SF_CREATE, $sf_mapping->sync_triggers)) {
+        if ($sf_mapping->checkTriggers([SALESFORCE_MAPPING_SYNC_SF_CREATE])) {
           try {
             // Create entity from mapping object and field maps.
-            $entity_info = entity_get_info($sf_mapping->drupal_entity_type);
+            $entity_info = \Drupal::entityTypeManager()->getDefinition($sf_mapping->get('drupal_entity_type'));
 
             // Define values to pass to entity_create().
+            $entity_keys = $entity_info->getKeys();
             $values = [];
-            if (isset($entity_info['entity keys']['bundle']) &&
-              !empty($entity_info['entity keys']['bundle'])) {
-              $values[$entity_info['entity keys']['bundle']] = $sf_mapping->drupal_bundle;
+            if (isset($entity_keys['bundle']) &&
+              !empty($entity_keys['bundle'])) {
+              $values[$entity_keys['bundle']] = $sf_mapping->get('drupal_bundle');
             }
             else {
               // Not all entities will have bundle defined under entity keys,
               // e.g. the User entity.
-              $values[$sf_mapping->drupal_bundle] = $sf_mapping->drupal_bundle;
+              $values[$sf_mapping->get('drupal_bundle')] = $sf_mapping->get('drupal_bundle');
             }
 
             // See note above about flag.
             $values['salesforce_pull'] = TRUE;
 
             // Create entity.
-            $entity = entity_create($sf_mapping->drupal_entity_type, $values);
+            $entity = \Drupal::entityTypeManager()
+              ->getStorage($sf_mapping->get('drupal_entity_type'))
+              ->create($values);
 
             // Flag this entity as having been processed. This does not persist,
             // but is used by salesforce_push to avoid duplicate processing.
             $entity->salesforce_pull = TRUE;
 
-            $wrapper = entity_metadata_wrapper($sf_mapping->drupal_entity_type, $entity);
-            salesforce_pull_map_fields($sf_mapping->field_mappings, $wrapper, $sf_object);
-            $wrapper->save();
+            //$wrapper = entity_metadata_wrapper($sf_mapping->drupal_entity_type, $entity);
+            $this->mapFields($sf_mapping, $entity, $sf_object);
+            $entity->save();
 
             // If no id exists, the insert failed.
-            list($entity_id) = entity_extract_ids($sf_mapping->drupal_entity_type, $entity);
-            if (!$entity_id) {
+            //list($entity_id) = entity_extract_ids($sf_mapping->drupal_entity_type, $entity);
+            if (!$entity->id()) {
               throw new Exception('Entity ID not returned, insert failed.');
             }
 
@@ -161,4 +179,66 @@ abstract class PullBase extends QueueWorkerBase {
       }
     }
   }
+
+  /**
+   * Map field values.
+   *
+   * @param object $sf_mapping
+   *   Array of field maps.
+   * @param object $entity
+   *   Entity wrapper object.
+   * @param object $sf_object
+   *   Object of the Salesforce record.
+   * @TODO this should move into SalesforceMapping.php
+   */
+  function mapFields(SalesforceMapping $sf_mapping, EntityInterface &$entity, $sf_object) {
+    $foo = $sf_mapping->getPullFields($entity);
+    $bar = $sf_mapping->get('field_mappings');
+
+    // Field plugin crib sheet
+    //$value = $sf_object[$field->get('salesforce_field')];
+    //$drupal_field = $field->get('drupal_field_value');
+
+    foreach ($sf_mapping->getPullFields($entity) as $field_map) {
+      // $poop = $field_map->get('drupal_field_value');
+      // $drupal_fields_array = explode(':', $field_map->get('drupal_field_value'));
+      // $parent = $entity;
+      $mapping_field_plugin_id = $field_map->get('drupal_field_type');
+      $mapping_field_plugin = $this->pluginManager->create($mapping_field_plugin_id, $field_map);
+
+      // $drupal_field_value = $field_map->get('drupal_field_value');
+        
+      try {
+        $value = $mapping_field_plugin->getPullValue($entity);
+      }
+      catch (Exception $e) {
+        watchdog_exception('sfpull', $e);
+        continue;
+      }
+
+      // @TODO: make this work for reference fields. There must be a better way than a semi-colon delimited string to represent this.
+      // It should look more like this in the future:
+      // $drupal_field->getValue($entity);
+
+      // Traverse through the field_value identifier to the child-most element. Practically this is in order to fine referenced entities. Right now we're ignoring that those exist and assuming that the field will have a value.
+      // foreach ($drupal_fields_array as $drupal_field) {
+      // }
+
+      // $fieldmap_type = salesforce_mapping_get_fieldmap_types($field_map->get('drupal_field_type'));
+      // $value = call_user_func($fieldmap_type['pull_value_callback'], $parent, $sf_object, $field_map);
+
+      // Allow this value to be altered before assigning to the entity.
+      drupal_alter('salesforce_pull_entity_value', $value, $field_map, $sf_object);
+      // if (isset($value)) {
+      //   // @TODO: might wrongly assumes an individual value wouldn't be an
+      //   // array.
+      //   if ($parent instanceof EntityListWrapper && !is_array($value)) {
+      //     $parent->offsetSet(0, $value);
+      //   }
+      //   else {
+      //     $parent->set($value);
+      //   }
+      }
+    }
+  }
 }
diff --git a/modules/salesforce_push/salesforce_push.module b/modules/salesforce_push/salesforce_push.module
index 6297f87b8c8539c002692dcf95aca0626111d42a..adb9721b9269e076e5f920950f4b6632b9805fb0 100644
--- a/modules/salesforce_push/salesforce_push.module
+++ b/modules/salesforce_push/salesforce_push.module
@@ -5,8 +5,8 @@
  * Push updates to Salesforce when a Drupal entity is updated.
  */
 
-use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\salesforce_mapping\Entity\MappedObject;
 use Drupal\salesforce_mapping\Entity\MappedObjectInterface;
 use Drupal\salesforce_mapping\Entity\SalesforceMapping;
@@ -37,7 +37,7 @@ function salesforce_push_entity_delete(EntityInterface $entity) {
 function salesforce_push_salesforce_push_entity_allowed(EntityInterface $entity, $op) {
   // Don't allow mapped objects or mappings to be pushed!
   // @TODO can we implement this instead with a validation constraint? This is fugly.
-  if ($entity instanceof MappedObjectInterface 
+  if ($entity instanceof MappedObjectInterface
   || $entity instanceof SalesforceMappingInterface) {
     return FALSE;
   }
@@ -54,10 +54,11 @@ function salesforce_push_salesforce_push_entity_allowed(EntityInterface $entity,
  * @TODO
  *   at some point all these hook_entity_* implementations will go away. We'll
  *   create an event subscriber class to respond to entity events and delegate
- *   actions to the appropriate Push procedures. Unfortunately this point seems 
+ *   actions to the appropriate Push procedures. Unfortunately this point seems
  *   to be a very long ways away. https://www.drupal.org/node/2551893
  */
 function salesforce_push_entity_crud(EntityInterface $entity, $op) {
+
   try {
     $mappings = salesforce_mapping_load_by_drupal($entity->getEntityTypeId());
   }
@@ -69,7 +70,7 @@ function salesforce_push_entity_crud(EntityInterface $entity, $op) {
   foreach ($mappings as $mapping) {
     $mapped_objects = [];
     $mapped_object = FALSE;
-    if (!$mapping->doesCrud([$op])) {
+    if (!$mapping->checkTriggers([$op])) {
       continue;
     }
     // @TODO decide whether this hook is worth moving to Events framework, and how. Should subscribers throw an exception to prevent entity sync? Return false, like so? Something else entirely?
diff --git a/salesforce.libraries.yml b/salesforce.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..042c110ac06912fff473e716747c36a2837caac3
--- /dev/null
+++ b/salesforce.libraries.yml
@@ -0,0 +1,6 @@
+admin:
+  version: 0.1
+  css:
+    layout:
+      css/salesforce.css: {}
+
diff --git a/src/Rest/RestClient.php b/src/Rest/RestClient.php
index 0f34e0ba3f023c8a54516cb9791e7c0453dac487..73640158c4a8fe8cbd76b6923f8dac3b105613ae 100644
--- a/src/Rest/RestClient.php
+++ b/src/Rest/RestClient.php
@@ -87,7 +87,7 @@ class RestClient {
     }
     catch (RequestException $e) {
       // RequestException gets thrown for any response status but 2XX
-      $this->response = new RestResponse($e->getResponse());
+      $this->response = $e->getResponse();
     }
     if (!is_object($this->response)) {
       throw new Exception('Unknown error occurred during API call');
@@ -100,10 +100,10 @@ class RestClient {
         // throws anything but a RequestException, let it bubble up.
         $this->refreshToken();
         try {
-          $this->response = $this->apiHttpRequest($path, $params, $method);
+          $this->response = new RestResponse($this->apiHttpRequest($path, $params, $method));
         }
         catch (RequestException $e) {
-          $this->response = new RestResponse($e->getResponse());
+          $this->response = $e->getResponse();
           throw $e;
         }
         break;
@@ -601,7 +601,7 @@ class RestClient {
    * @addtogroup salesforce_apicalls
    */
   public function objectRead($name, $id) {
-    return new SObject($this->apiCall("sobjects/{$name}/{$id}", [], 'GET'));
+    return new SObject($this->apiCall("sobjects/{$name}/{$id}"));
   }
 
   /**
diff --git a/src/SalesforceEvents.php b/src/SalesforceEvents.php
new file mode 100644
index 0000000000000000000000000000000000000000..6352393ce256b38874c21bc31648ca0503256006
--- /dev/null
+++ b/src/SalesforceEvents.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\salesforce;
+
+/**
+ * Defines events for Salesforce
+ *
+ * @see \Drupal\salesforce\SalesforceEvent
+ */
+final class SalesforceEvents {
+
+  /**
+   * Name of the event fired when building params to push to Salesforce.
+   *
+   * This event allows modules to add, change, or remove params before they're 
+   * pushed to Salesforce. The event listener method receives a
+   * \Drupal\salesforce\SalesforceEvent instance.
+   * Previously hook_salesforce_push_params_alter()
+   *
+   * @Event
+   *
+   * @var string
+   */
+  const PUSH_PARAMS = 'salesforce.push_params';
+
+  /**
+   * hook_salesforce_push_entity_allowed
+   */
+  const PUSH_CRUD_ALLOWED = 'salesforce.push_crud.allowed';
+
+  /**
+    * hook_salesforce_push_success
+    */
+  const PUSH_SUCCESS = 'salesforce.push_success';
+
+  /**
+   * hook_salesforce_push_fail
+   */
+  const PUSH_FAIL = 'salesforce.push_fail';  
+
+  /**
+   * hook_salesforce_pull_entity_presave
+   */
+  const PULL_PRESAVE = 'salesforce.pull_presave';
+
+  /**
+   * hook_salesforce_pull_entity_insert
+   */
+  const PULL_INSERT = 'salesforce.pull_insert';
+
+  /**
+   * hook_salesforce_pull_entity_update
+   */
+  const PULL_UPDATE = 'salesforce.pull_update';
+
+}