diff --git a/openbis_ng_ui/src/js/common/consts/ids.js b/openbis_ng_ui/src/js/common/consts/ids.js
index 54947bbc7613256812696e0e6321e2c7bf8f4c19..0861f9e06075a0fc0f3d0fbb4a3fabf009ee182b 100644
--- a/openbis_ng_ui/src/js/common/consts/ids.js
+++ b/openbis_ng_ui/src/js/common/consts/ids.js
@@ -1,5 +1,7 @@
 // app
 const WEB_APP_ID = 'openbis_ng_ui'
+const WEB_APP_SERVICE = 'openbis-ng-ui-service'
+const EXPORT_SERVICE = 'xls-export'
 
 // grids
 const OBJECT_TYPES_GRID_ID = 'object_types_grid'
@@ -31,6 +33,10 @@ export default {
   // app
   WEB_APP_ID,
 
+  // service,
+  WEB_APP_SERVICE,
+  EXPORT_SERVICE,
+
   // grids
   OBJECT_TYPES_GRID_ID,
   COLLECTION_TYPES_GRID_ID,
diff --git a/openbis_ng_ui/src/js/components/common/grid/Grid.jsx b/openbis_ng_ui/src/js/components/common/grid/Grid.jsx
index c54e15a602de2c59d7b21585c2cdead9801a711f..ecb936950d3997342b9384ff92cbad09f71b2e71 100644
--- a/openbis_ng_ui/src/js/components/common/grid/Grid.jsx
+++ b/openbis_ng_ui/src/js/components/common/grid/Grid.jsx
@@ -224,13 +224,17 @@ class Grid extends React.PureComponent {
   }
 
   renderExports() {
-    const { id, multiselectable } = this.props
+    const { id, exportable, multiselectable } = this.props
     const { rows, exportOptions } = this.state
 
+    if (!exportable) {
+      return null
+    }
+
     return (
       <GridExports
         id={id}
-        disabled={rows.length === 0}
+        disabled={!exportable || rows.length === 0}
         exportOptions={exportOptions}
         multiselectable={multiselectable}
         onExport={this.controller.handleExport}
diff --git a/openbis_ng_ui/src/js/components/common/grid/GridController.js b/openbis_ng_ui/src/js/components/common/grid/GridController.js
index 19f1578c3c23bb67ccc2d230cf7c1d0171cfdcf3..e43161b6b7169ab24e17a41cb26423a8fbc6858a 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridController.js
+++ b/openbis_ng_ui/src/js/components/common/grid/GridController.js
@@ -1,7 +1,5 @@
 import _ from 'lodash'
 import autoBind from 'auto-bind'
-import FileSaver from 'file-saver'
-import CsvStringify from 'csv-stringify'
 import GridFilterOptions from '@src/js/components/common/grid/GridFilterOptions.js'
 import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js'
 import GridPagingOptions from '@src/js/components/common/grid/GridPagingOptions.js'
@@ -1022,122 +1020,106 @@ export default class GridController {
   }
 
   async handleExport() {
-    const { exportOptions } = this.context.getState()
-
-    function _stringToUtf16ByteArray(str) {
-      var bytes = []
-      bytes.push(255, 254)
-      for (var i = 0; i < str.length; ++i) {
-        var charCode = str.charCodeAt(i)
-        bytes.push(charCode & 0xff) //low byte
-        bytes.push((charCode & 0xff00) >>> 8) //high byte (might be 0)
-      }
-      return bytes
-    }
-
-    function _exportColumnsFromData(namePrefix, rows, columns) {
-      const arrayOfRowArrays = []
-
-      const headers = columns.map(column => column.name)
-      arrayOfRowArrays.push(headers)
-
-      rows.forEach(row => {
-        var rowAsArray = []
-        columns.forEach(column => {
-          var rowValue = column.getValue({
-            row,
-            column,
-            operation: 'export',
-            exportOptions
-          })
-          if (!rowValue) {
-            rowValue = ''
-          } else {
-            var specialCharsRemover = document.createElement('textarea')
-            specialCharsRemover.innerHTML = rowValue
-            rowValue = specialCharsRemover.value //Removes special HTML Chars
-            rowValue = String(rowValue).replace(/\r?\n|\r|\t/g, ' ') //Remove carriage returns and tabs
-
-            if (exportOptions.values === GridExportOptions.RICH_TEXT) {
-              // do nothing with the value
-            } else if (exportOptions.values === GridExportOptions.PLAIN_TEXT) {
-              rowValue = String(rowValue).replace(/<(?:.|\n)*?>/gm, '')
-            } else {
-              throw Error('Unsupported values option: ' + exportOptions.values)
-            }
-          }
-          rowAsArray.push(rowValue)
-        })
-        arrayOfRowArrays.push(rowAsArray)
-      })
-
-      CsvStringify(
-        {
-          header: false,
-          delimiter: '\t',
-          quoted: false
-        },
-        arrayOfRowArrays,
-        function (err, tsv) {
-          var utf16bytes = _stringToUtf16ByteArray(tsv)
-          var utf16bytesArray = new Uint8Array(utf16bytes.length)
-          utf16bytesArray.set(utf16bytes, 0)
-          var blob = new Blob([utf16bytesArray], {
-            type: 'text/tsv;charset=UTF-16LE;'
-          })
-          FileSaver.saveAs(blob, 'exportedTable' + namePrefix + '.tsv')
-        }
-      )
-    }
-
     const state = this.context.getState()
     const props = this.context.getProps()
 
-    var data = []
-    var columns = []
-    var prefix = ''
+    const { scheduleExport, loadExported } = props
 
-    if (exportOptions.columns === GridExportOptions.ALL_COLUMNS) {
-      columns = this.getAllColumns()
-      prefix += 'AllColumns'
-    } else if (exportOptions.columns === GridExportOptions.VISIBLE_COLUMNS) {
-      columns = this.getVisibleColumns()
-      prefix += 'VisibleColumns'
-    } else {
-      throw Error('Unsupported columns option: ' + exportOptions.columns)
+    if (!scheduleExport || !loadExported) {
+      return
     }
 
-    columns = columns.filter(column => column.exportable)
+    let exportedRows = []
 
-    if (exportOptions.rows === GridExportOptions.ALL_PAGES) {
+    if (state.exportOptions.rows === GridExportOptions.ALL_PAGES) {
       if (state.local) {
-        data = state.sortedRows
+        exportedRows = state.sortedRows
       } else if (props.loadRows) {
+        const columns = {}
+
+        state.allColumns.forEach(column => {
+          columns[column.name] = column
+        })
+
         const loadedResult = await props.loadRows({
+          columns: columns,
+          filterMode: state.filterMode,
           filters: state.filters,
           globalFilter: state.globalFilter,
           page: 0,
           pageSize: 1000000,
           sortings: state.sortings
         })
-        data = loadedResult.rows
+        exportedRows = loadedResult.rows
       }
-
-      prefix += 'AllPages'
-      _exportColumnsFromData(prefix, data, columns)
-    } else if (exportOptions.rows === GridExportOptions.CURRENT_PAGE) {
-      data = state.rows
-      prefix += 'CurrentPage'
-      _exportColumnsFromData(prefix, data, columns)
-    } else if (exportOptions.rows === GridExportOptions.SELECTED_ROWS) {
-      data = Object.values(state.multiselectedRows).map(
+    } else if (state.exportOptions.rows === GridExportOptions.CURRENT_PAGE) {
+      exportedRows = state.rows
+    } else if (state.exportOptions.rows === GridExportOptions.SELECTED_ROWS) {
+      exportedRows = Object.values(state.multiselectedRows).map(
         selectedRow => selectedRow.data
       )
-      prefix += 'SelectedRows'
-      _exportColumnsFromData(prefix, data, columns)
     } else {
-      throw Error('Unsupported rows option: ' + exportOptions.columns)
+      throw Error('Unsupported rows option: ' + state.exportOptions.columns)
+    }
+
+    if (exportedRows.some(row => _.isEmpty(row.exportableId))) {
+      throw Error(
+        "Some of the rows to be exported do not have 'exportableId' set."
+      )
     }
+
+    let exportedProperties = {}
+
+    if (state.exportOptions.columns === GridExportOptions.ALL_COLUMNS) {
+      exportedProperties = {}
+    } else if (
+      state.exportOptions.columns === GridExportOptions.VISIBLE_COLUMNS
+    ) {
+      const { newAllColumns } = await this._loadColumns(
+        exportedRows,
+        state.columnsVisibility,
+        state.columnsSorting
+      )
+
+      newAllColumns.forEach(column => {
+        if (column.exportableProperty) {
+          const propertyCode = column.exportableProperty.code
+          const propertyTypesMap = column.exportableProperty.types
+
+          Object.keys(propertyTypesMap).forEach(kind => {
+            const propertyTypesForKind = propertyTypesMap[kind]
+
+            propertyTypesForKind.forEach(propertyTypePermId => {
+              let exportedPropertiesForKind = exportedProperties[kind]
+
+              if (!exportedPropertiesForKind) {
+                exportedProperties[kind] = exportedPropertiesForKind = {}
+              }
+
+              let exportedPropertiesForKindAndType =
+                exportedPropertiesForKind[propertyTypePermId]
+
+              if (!exportedPropertiesForKindAndType) {
+                exportedPropertiesForKind[propertyTypePermId] =
+                  exportedPropertiesForKindAndType = []
+              }
+
+              exportedPropertiesForKindAndType.push(propertyCode)
+            })
+          })
+        }
+      })
+    } else {
+      throw Error('Unsupported columns option: ' + state.exportOptions.columns)
+    }
+
+    const exportedIds = exportedRows.map(row => row.exportableId)
+
+    scheduleExport({
+      exportedIds: exportedIds,
+      exportedProperties: exportedProperties,
+      exportedValues: state.exportOptions.values
+    })
   }
 
   async handleExportOptionsChange(exportOptions) {
diff --git a/openbis_ng_ui/src/js/components/common/grid/GridWithSettings.jsx b/openbis_ng_ui/src/js/components/common/grid/GridWithSettings.jsx
index 5e1ef524efb97f30c0cb1afee4b0720b815252a2..c35a1f01a380e4758cd297c1983cb645cdf76bee 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridWithSettings.jsx
+++ b/openbis_ng_ui/src/js/components/common/grid/GridWithSettings.jsx
@@ -1,6 +1,7 @@
 import React from 'react'
 import autoBind from 'auto-bind'
 import Grid from '@src/js/components/common/grid/Grid.jsx'
+import GridExportOptions from '@src/js/components/common/grid/GridExportOptions.js'
 import openbis from '@src/js/services/openbis.js'
 import ids from '@src/js/common/consts/ids.js'
 import logger from '@src/js/common/logger.js'
@@ -22,6 +23,8 @@ export default class GridWithSettings extends React.PureComponent {
         {...this.props}
         loadSettings={this.loadSettings}
         onSettingsChange={this.onSettingsChange}
+        scheduleExport={this.props.exportable ? this.scheduleExport : null}
+        loadExported={this.props.exportable ? this.loadExported : null}
       />
     )
   }
@@ -59,4 +62,27 @@ export default class GridWithSettings extends React.PureComponent {
 
     await openbis.updatePersons([update])
   }
+
+  async scheduleExport({ exportedIds, exportedProperties, exportedValues }) {
+    const serviceId = new openbis.CustomASServiceCode(ids.EXPORT_SERVICE)
+
+    const serviceOptions = new openbis.CustomASServiceExecutionOptions()
+    serviceOptions.withParameter('method', 'export')
+    serviceOptions.withParameter('file_name', this.props.id)
+    serviceOptions.withParameter('ids', exportedIds)
+    serviceOptions.withParameter('export_referred', true)
+    serviceOptions.withParameter('export_properties', exportedProperties)
+
+    if (exportedValues === GridExportOptions.PLAIN_TEXT) {
+      serviceOptions.withParameter('text_formatting', 'PLAIN')
+    } else if (exportedValues === GridExportOptions.RICH_TEXT) {
+      serviceOptions.withParameter('text_formatting', 'RICH')
+    } else {
+      throw Error('Unsupported text formatting ' + exportedValues)
+    }
+
+    return await openbis.executeService(serviceId, serviceOptions)
+  }
+
+  async loadExported() {}
 }
diff --git a/openbis_ng_ui/src/js/components/tools/form/activeUserReport/ActiveUserReportFacade.js b/openbis_ng_ui/src/js/components/tools/form/activeUserReport/ActiveUserReportFacade.js
index 8916c740266435182460e267787d94bb0e5b527a..29244e8a6a4966b37349d687c7b15f98f07f4a88 100644
--- a/openbis_ng_ui/src/js/components/tools/form/activeUserReport/ActiveUserReportFacade.js
+++ b/openbis_ng_ui/src/js/components/tools/form/activeUserReport/ActiveUserReportFacade.js
@@ -1,8 +1,9 @@
 import openbis from '@src/js/services/openbis.js'
+import ids from '@src/js/common/consts/ids.js'
 
 export default class ActiveUserReportFacade {
   async sendReport() {
-    const serviceId = new openbis.CustomASServiceCode('openbis-ng-ui-service')
+    const serviceId = new openbis.CustomASServiceCode(ids.WEB_APP_SERVICE)
     const serviceOptions = new openbis.CustomASServiceExecutionOptions()
     serviceOptions.withParameter('method', 'sendCountActiveUsersEmail')
     return openbis.executeService(serviceId, serviceOptions)
diff --git a/openbis_ng_ui/src/js/components/types/common/EntityTypesGrid.jsx b/openbis_ng_ui/src/js/components/types/common/EntityTypesGrid.jsx
index ca7e09d3b7847f4e6a148e930e5412d625083c6c..1a0003e327cd103bc0da3adbf86db99281779ea4 100644
--- a/openbis_ng_ui/src/js/components/types/common/EntityTypesGrid.jsx
+++ b/openbis_ng_ui/src/js/components/types/common/EntityTypesGrid.jsx
@@ -10,8 +10,14 @@ class EntityTypesGrid extends React.PureComponent {
   render() {
     logger.log(logger.DEBUG, 'EntityTypesGrid.render')
 
-    const { id, rows, selectedRowId, onSelectedRowChange, controllerRef } =
-      this.props
+    const {
+      id,
+      rows,
+      exportable,
+      selectedRowId,
+      onSelectedRowChange,
+      controllerRef
+    } = this.props
 
     return (
       <GridWithSettings
@@ -21,6 +27,7 @@ class EntityTypesGrid extends React.PureComponent {
         columns={this.getColumns()}
         rows={rows}
         sort='code'
+        exportable={exportable}
         selectable={true}
         selectedRowId={selectedRowId}
         onSelectedRowChange={onSelectedRowChange}
diff --git a/openbis_ng_ui/src/js/components/types/common/PropertyTypesGrid.jsx b/openbis_ng_ui/src/js/components/types/common/PropertyTypesGrid.jsx
index 1b879db7380652004fe3e866d6e006eadc837db9..2fa5b20155ec90946e3cc64d49a778d42a0ddaf5 100644
--- a/openbis_ng_ui/src/js/components/types/common/PropertyTypesGrid.jsx
+++ b/openbis_ng_ui/src/js/components/types/common/PropertyTypesGrid.jsx
@@ -104,6 +104,7 @@ class PropertyTypesGrid extends React.PureComponent {
         ]}
         rows={rows}
         sort='code'
+        exportable={false}
         selectable={true}
         selectedRowId={selectedRowId}
         onSelectedRowChange={onSelectedRowChange}
diff --git a/openbis_ng_ui/src/js/components/types/common/VocabularyTypesGrid.jsx b/openbis_ng_ui/src/js/components/types/common/VocabularyTypesGrid.jsx
index 34ab388f9c33b788dbb15a1f2e4aad03cb8f7eb7..68260c1ab9cf3437afb25ec25b3ddb4183a6775a 100644
--- a/openbis_ng_ui/src/js/components/types/common/VocabularyTypesGrid.jsx
+++ b/openbis_ng_ui/src/js/components/types/common/VocabularyTypesGrid.jsx
@@ -38,6 +38,7 @@ class VocabularyTypesGrid extends React.PureComponent {
         ]}
         rows={rows}
         sort='code'
+        exportable={true}
         selectable={true}
         selectedRowId={selectedRowId}
         onSelectedRowChange={onSelectedRowChange}
diff --git a/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx b/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx
index 35ead6de702997109cfc2926404b4f6b85b1e65d..91e2f83ea6f7c929a109e36ba01230bba9f69029 100644
--- a/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx
+++ b/openbis_ng_ui/src/js/components/types/search/TypeSearch.jsx
@@ -73,6 +73,10 @@ class TypeSearch extends React.Component {
       .filter(result.objects, this.props.searchText, ['code', 'description'])
       .map(object => ({
         id: _.get(object, 'code'),
+        exportableId: {
+          exportable_kind: 'SAMPLE_TYPE',
+          perm_id: object.getPermId().getPermId()
+        },
         code: _.get(object, 'code'),
         description: _.get(object, 'description'),
         subcodeUnique: _.get(object, 'subcodeUnique', false),
@@ -103,6 +107,10 @@ class TypeSearch extends React.Component {
       .filter(result.objects, this.props.searchText, ['code', 'description'])
       .map(object => ({
         id: _.get(object, 'code'),
+        exportableId: {
+          exportable_kind: 'EXPERIMENT_TYPE',
+          perm_id: object.getPermId().getPermId()
+        },
         code: _.get(object, 'code'),
         description: _.get(object, 'description'),
         validationPlugin: _.get(object, 'validationPlugin.name')
@@ -130,6 +138,10 @@ class TypeSearch extends React.Component {
       .filter(result.objects, this.props.searchText, ['code', 'description'])
       .map(object => ({
         id: _.get(object, 'code'),
+        exportableId: {
+          exportable_kind: 'DATASET_TYPE',
+          perm_id: object.getPermId().getPermId()
+        },
         code: _.get(object, 'code'),
         description: _.get(object, 'description'),
         validationPlugin: _.get(object, 'validationPlugin.name'),
@@ -184,6 +196,10 @@ class TypeSearch extends React.Component {
       .filter(result.objects, this.props.searchText, ['code', 'description'])
       .map(object => ({
         id: object.code,
+        exportableId: {
+          exportable_kind: 'VOCABULARY',
+          perm_id: object.getPermId().getPermId()
+        },
         code: object.code,
         description: object.description,
         urlTemplate: object.urlTemplate
@@ -382,6 +398,7 @@ class TypeSearch extends React.Component {
             }
             kind={openbis.EntityKind.SAMPLE}
             rows={this.state.objectTypes}
+            exportable={true}
             onSelectedRowChange={this.handleSelectedRowChange(
               objectTypes.OBJECT_TYPE
             )}
@@ -407,6 +424,7 @@ class TypeSearch extends React.Component {
             }
             kind={openbis.EntityKind.EXPERIMENT}
             rows={this.state.collectionTypes}
+            exportable={true}
             onSelectedRowChange={this.handleSelectedRowChange(
               objectTypes.COLLECTION_TYPE
             )}
@@ -430,6 +448,7 @@ class TypeSearch extends React.Component {
             }
             kind={openbis.EntityKind.DATA_SET}
             rows={this.state.dataSetTypes}
+            exportable={true}
             onSelectedRowChange={this.handleSelectedRowChange(
               objectTypes.DATA_SET_TYPE
             )}
@@ -455,6 +474,7 @@ class TypeSearch extends React.Component {
             }
             kind={openbis.EntityKind.MATERIAL}
             rows={this.state.materialTypes}
+            exportable={false}
             onSelectedRowChange={this.handleSelectedRowChange(
               objectTypes.MATERIAL_TYPE
             )}