diff --git a/docs/scripting-api.rst b/docs/scripting-api.rst
index 28cf09dc979e05a2c9730d22fb90dff7d3805524..082410016fa5ad1bf6b3b915ee7130378d08efaf 100644
--- a/docs/scripting-api.rst
+++ b/docs/scripting-api.rst
@@ -232,6 +232,10 @@ omitted.
        copy(mimeText, 'Hello, World!',
             mimeHtml, '<p>Hello, World!</p>')
 
+.. js:function:: copy(Item)
+
+   Function override with an item argument.
+
 .. js:function:: copy()
 
    Sends ``Ctrl+C`` to current window.
@@ -324,11 +328,11 @@ omitted.
 
    Copies previous item from current tab to clipboard.
 
-.. js:function:: add(text|item...)
+.. js:function:: add(text|Item...)
 
    Same as ``insert(0, ...)``.
 
-.. js:function:: insert(row, text|item...)
+.. js:function:: insert(row, text|Item...)
 
    Inserts new items to current tab.
 
@@ -366,12 +370,28 @@ omitted.
 
    Throws an exception if space for the items cannot be allocated.
 
+.. js:function:: write(row, Item...)
+
+   Function override with one or more item arguments.
+
+.. js:function:: write(row, Item[])
+
+   Function override with item list argument.
+
 .. js:function:: change(row, mimeType, data, [mimeType, data]...)
 
    Changes data in item in current tab.
 
    If data is ``undefined`` the format is removed from item.
 
+.. js:function:: change(row, Item...)
+
+   Function override with one or more item arguments.
+
+.. js:function:: change(row, Item[])
+
+   Function override with item list argument.
+
 .. js:function:: String separator()
 
    Returns item separator (used when concatenating item data).
@@ -643,7 +663,7 @@ omitted.
 
    See `Selected Items`_.
 
-.. js:function:: bool setSelectedItemData(index, item)
+.. js:function:: bool setSelectedItemData(index, Item)
 
    Set data for given selected item.
 
@@ -661,7 +681,7 @@ omitted.
 
    See `Selected Items`_.
 
-.. js:function:: setSelectedItemsData(item[])
+.. js:function:: setSelectedItemsData(Item[])
 
    Set data to all selected items.
 
@@ -684,7 +704,7 @@ omitted.
 
    Returns deserialized object from serialized items.
 
-.. js:function:: ByteArray pack(item)
+.. js:function:: ByteArray pack(Item)
 
    Returns serialized item.
 
@@ -692,7 +712,7 @@ omitted.
 
    Returns an item in current tab.
 
-.. js:function:: setItem(row, text|item)
+.. js:function:: setItem(row, text|Item)
 
    Inserts item to current tab.
 
diff --git a/src/gui/commandcompleterdocumentation.h b/src/gui/commandcompleterdocumentation.h
index 4414c443aa551c53dea0dbdc3aa922b02637ef2b..b24efa6f4b9c527a7b7e4b40acf6dfeca51ded57 100644
--- a/src/gui/commandcompleterdocumentation.h
+++ b/src/gui/commandcompleterdocumentation.h
@@ -32,6 +32,7 @@ void addDocumentation(AddDocumentationCallback addDocumentation)
     addDocumentation("isClipboard", "bool isClipboard()", "Returns true only in automatic command triggered by clipboard change.");
     addDocumentation("copy", "copy(text)", "Sets clipboard plain text.");
     addDocumentation("copy", "copy(mimeType, data, [mimeType, data]...)", "Sets clipboard data.");
+    addDocumentation("copy", "copy(Item)", "Function override with an item argument.");
     addDocumentation("copy", "copy()", "Sends `Ctrl+C` to current window.");
     addDocumentation("copySelection", "copySelection(...)", "Same as `copy(...)` for Linux/X11 mouse selection.");
     addDocumentation("paste", "paste()", "Pastes current clipboard.");
@@ -47,15 +48,19 @@ void addDocumentation(AddDocumentationCallback addDocumentation)
     addDocumentation("select", "select(row)", "Copies item in the row to clipboard.");
     addDocumentation("next", "next()", "Copies next item from current tab to clipboard.");
     addDocumentation("previous", "previous()", "Copies previous item from current tab to clipboard.");
-    addDocumentation("add", "add(text|item...)", "Same as `insert(0, ...)`.");
-    addDocumentation("insert", "insert(row, text|item...)", "Inserts new items to current tab.");
+    addDocumentation("add", "add(text|Item...)", "Same as `insert(0, ...)`.");
+    addDocumentation("insert", "insert(row, text|Item...)", "Inserts new items to current tab.");
     addDocumentation("remove", "remove(row, ...)", "Removes items in current tab.");
     addDocumentation("move", "move(row)", "Moves selected items to given row in same tab.");
     addDocumentation("edit", "edit([row|text] ...)", "Edits items in current tab.");
     addDocumentation("read", "ByteArray read([mimeType])", "Same as `clipboard()`.");
     addDocumentation("read", "ByteArray read(mimeType, row, ...)", "Returns concatenated data from items, or clipboard if row is negative.");
     addDocumentation("write", "write(row, mimeType, data, [mimeType, data]...)", "Inserts new item to current tab.");
+    addDocumentation("write", "write(row, Item...)", "Function override with one or more item arguments.");
+    addDocumentation("write", "write(row, Item[])", "Function override with item list argument.");
     addDocumentation("change", "change(row, mimeType, data, [mimeType, data]...)", "Changes data in item in current tab.");
+    addDocumentation("change", "change(row, Item...)", "Function override with one or more item arguments.");
+    addDocumentation("change", "change(row, Item[])", "Function override with item list argument.");
     addDocumentation("separator", "String separator()", "Returns item separator (used when concatenating item data).");
     addDocumentation("separator", "separator(separator)", "Sets item separator for concatenating item data.");
     addDocumentation("action", "action()", "Opens action dialog.");
@@ -95,15 +100,15 @@ void addDocumentation(AddDocumentationCallback addDocumentation)
     addDocumentation("selectedTab", "String selectedTab()", "Returns tab that was selected when script was executed.");
     addDocumentation("selectedItems", "int[] selectedItems()", "Returns selected rows in current tab.");
     addDocumentation("selectedItemData", "Item selectedItemData(index)", "Returns data for given selected item.");
-    addDocumentation("setSelectedItemData", "bool setSelectedItemData(index, item)", "Set data for given selected item.");
+    addDocumentation("setSelectedItemData", "bool setSelectedItemData(index, Item)", "Set data for given selected item.");
     addDocumentation("selectedItemsData", "Item[] selectedItemsData()", "Returns data for all selected items.");
-    addDocumentation("setSelectedItemsData", "setSelectedItemsData(item[])", "Set data to all selected items.");
+    addDocumentation("setSelectedItemsData", "setSelectedItemsData(Item[])", "Set data to all selected items.");
     addDocumentation("currentItem", "int currentItem(), int index()", "Returns current row in current tab.");
     addDocumentation("escapeHtml", "String escapeHtml(text)", "Returns text with special HTML characters escaped.");
     addDocumentation("unpack", "Item unpack(data)", "Returns deserialized object from serialized items.");
-    addDocumentation("pack", "ByteArray pack(item)", "Returns serialized item.");
+    addDocumentation("pack", "ByteArray pack(Item)", "Returns serialized item.");
     addDocumentation("getItem", "Item getItem(row)", "Returns an item in current tab.");
-    addDocumentation("setItem", "setItem(row, text|item)", "Inserts item to current tab.");
+    addDocumentation("setItem", "setItem(row, text|Item)", "Inserts item to current tab.");
     addDocumentation("toBase64", "String toBase64(data)", "Returns base64-encoded data.");
     addDocumentation("fromBase64", "ByteArray fromBase64(base64String)", "Returns base64-decoded data.");
     addDocumentation("md5sum", "ByteArray md5sum(data)", "Returns MD5 checksum of data.");
diff --git a/src/scriptable/scriptable.cpp b/src/scriptable/scriptable.cpp
index 7c7d551733861bfe9e399cb62db5534598edcf94..4aa3e73bc802944b06b5073af7c45412211730a3 100644
--- a/src/scriptable/scriptable.cpp
+++ b/src/scriptable/scriptable.cpp
@@ -624,16 +624,19 @@ Scriptable::Scriptable(
     addScriptableClass(&ScriptableDir::staticMetaObject, "Dir", m_engine);
 }
 
+QJSValue Scriptable::argumentsArray() const
+{
+    return m_engine->globalObject().property("_copyqArguments");
+}
+
 int Scriptable::argumentCount() const
 {
-    const auto args = m_engine->globalObject().property("_copyqArguments");
-    return args.property("length").toInt();
+    return argumentsArray().property("length").toInt();
 }
 
 QJSValue Scriptable::argument(int index) const
 {
-    const auto args = m_engine->globalObject().property("_copyqArguments");
-    return args.property(static_cast<quint32>(index) );
+    return argumentsArray().property(static_cast<quint32>(index) );
 }
 
 QJSValue Scriptable::newByteArray(const QByteArray &bytes) const
@@ -1306,16 +1309,16 @@ QJSValue Scriptable::read()
     return newByteArray(result);
 }
 
-void Scriptable::write()
+QJSValue Scriptable::write()
 {
     m_skipArguments = -1;
-    changeItem(true);
+    return changeItem(true);
 }
 
-void Scriptable::change()
+QJSValue Scriptable::change()
 {
     m_skipArguments = -1;
-    changeItem(false);
+    return changeItem(false);
 }
 
 void Scriptable::separator()
@@ -2931,16 +2934,77 @@ QVector<int> Scriptable::getRows() const
     return rows;
 }
 
+QVector<QVariantMap> Scriptable::getItemArguments(int begin, int end, QString *error)
+{
+    if (begin == end) {
+        *error = QLatin1String("Expected item arguments");
+        return {};
+    }
+
+    const auto firstArg = argument(begin);
+    if (firstArg.isArray()) {
+        if (end - begin != 1) {
+            *error = QLatin1String("Unexpected multiple item list arguments");
+            return {};
+        }
+        return getItemList(0, firstArg.property("length").toInt(), firstArg);
+    }
+
+    QVector<QVariantMap> items;
+    if (firstArg.toVariant().canConvert<QVariantMap>()) {
+        for (int i = begin; i < end; ++i)
+            items.append( fromScriptValue<QVariantMap>(argument(i), this) );
+    } else if (end - begin == 1) {
+        QVariantMap data;
+        QJSValue value = argument(begin);
+        setTextData( &data, toString(value) );
+        items.append(data);
+    } else {
+        if ((end - begin) % 2 != 0) {
+            *error = QLatin1String("Unexpected uneven number of mimeType/data arguments");
+            return {};
+        }
+        QVariantMap data;
+        for (int i = begin; i < end; i += 2) {
+            // MIME
+            const QString mime = toString(argument(i));
+            // DATA
+            toItemData( argument(i + 1), mime, &data );
+        }
+        items.append(data);
+    }
+
+    return items;
+}
+
+QVector<QVariantMap> Scriptable::getItemList(int begin, int end, const QJSValue &arguments)
+{
+    if (end < begin)
+        return {};
+
+    QVector<QVariantMap> items;
+    items.reserve(end - begin);
+
+    for (int i = begin; i < end; ++i) {
+        const auto arg = arguments.property( static_cast<quint32>(i) );
+        if ( arg.isObject() && getByteArray(arg) == nullptr && !arg.isArray() )
+            items.append( fromScriptValue<QVariantMap>(arg, this) );
+        else
+            items.append( createDataMap(mimeText, toString(arg)) );
+    }
+
+    return items;
+}
+
 QJSValue Scriptable::copy(ClipboardMode mode)
 {
     const int args = argumentCount();
-    QVariantMap data;
 
     if (args == 0) {
         // Reset clipboard first.
         const QString mime = COPYQ_MIME_PREFIX "invalid";
         const QByteArray value = "invalid";
-        data.insert(mime, value);
+        const QVariantMap data = createDataMap(mime, value);
         m_proxy->setClipboard(data, mode);
 
         m_proxy->copyFromCurrentWindow();
@@ -2955,27 +3019,16 @@ QJSValue Scriptable::copy(ClipboardMode mode)
         return throwError( tr("Failed to copy to clipboard!") );
     }
 
-    if (args == 1) {
-        QJSValue value = argument(0);
-        setTextData( &data, toString(value) );
-        m_proxy->setClipboard(data, mode);
-        return true;
-    }
-
-    if (args % 2 == 0) {
-        for (int i = 0; i < args; ++i) {
-            // MIME
-            QString mime = toString(argument(i));
+    QString error;
+    const QVector<QVariantMap> items = getItemArguments(0, args, &error);
+    if ( !error.isEmpty() )
+        return throwError(error);
 
-            // DATA
-            toItemData(argument(++i), mime, &data);
-        }
+    if (items.size() != 1)
+        return throwError(QLatin1String("Expected single item"));
 
-        m_proxy->setClipboard(data, mode);
-        return true;
-    }
-
-    return throwError(argumentError());
+    m_proxy->setClipboard(items[0], mode);
+    return true;
 }
 
 void Scriptable::abortEvaluation(Abort abort)
@@ -2985,7 +3038,7 @@ void Scriptable::abortEvaluation(Abort abort)
     emit finished();
 }
 
-void Scriptable::changeItem(bool create)
+QJSValue Scriptable::changeItem(bool create)
 {
     int row;
     int args = argumentCount();
@@ -2993,37 +3046,27 @@ void Scriptable::changeItem(bool create)
 
     // [ROW]
     if ( toInt(argument(0), &row) ) {
-        if (args < 3 || args % 2 != 1 ) {
-            throwError(argumentError());
-            return;
-        }
         i = 1;
     } else {
-        if (args < 2 || args % 2 != 0 ) {
-            throwError(argumentError());
-            return;
-        }
         row = 0;
         i = 0;
     }
 
-    QVariantMap data;
-
-    for (; i < args; i += 2) {
-        // MIME
-        const QString mime = toString(argument(i));
-        // DATA
-        toItemData( argument(i + 1), mime, &data );
-    }
+    QString error;
+    const QVector<QVariantMap> items = getItemArguments(i, args, &error);
+    if ( !error.isEmpty() )
+        return throwError(error);
 
     if (create) {
-        QVector<QVariantMap> items(1, data);
-        const auto error = m_proxy->browserInsert(m_tabName, row, items);
+        error = m_proxy->browserInsert(m_tabName, row, items);
         if ( !error.isEmpty() )
-            throwError(error);
+            return throwError(error);
     } else {
-        m_proxy->browserChange(m_tabName, data, row);
+        error = m_proxy->browserChange(m_tabName, row, items);
+        if ( !error.isEmpty() )
+            return throwError(error);
     }
+    return QJSValue();
 }
 
 void Scriptable::nextToClipboard(int where)
@@ -3325,17 +3368,7 @@ void Scriptable::insert(int row, int argumentsBegin, int argumentsEnd)
 {
     m_skipArguments = argumentsEnd;
 
-    QVector<QVariantMap> items;
-    items.reserve(argumentsEnd - argumentsBegin);
-
-    for (int i = argumentsBegin; i < argumentsEnd; ++i) {
-        const auto arg = argument(i);
-        if ( arg.isObject() && getByteArray(arg) == nullptr && !arg.isArray() )
-            items.append( fromScriptValue<QVariantMap>(arg, this) );
-        else
-            items.append( createDataMap(mimeText, toString(arg)) );
-    }
-
+    const QVector<QVariantMap> items = getItemList(argumentsBegin, argumentsEnd, argumentsArray());
     const auto error = m_proxy->browserInsert(m_tabName, row, items);
     if ( !error.isEmpty() )
         throwError(error);
diff --git a/src/scriptable/scriptable.h b/src/scriptable/scriptable.h
index 527438ceb9c04de2c5e96b7ebc48747c0f5aba26..93bf07da55b34ebcfafe7aaba4f4487f01f48d5c 100644
--- a/src/scriptable/scriptable.h
+++ b/src/scriptable/scriptable.h
@@ -84,6 +84,7 @@ public:
         AllEvaluations,
     };
 
+    QJSValue argumentsArray() const;
     int argumentCount() const;
     QJSValue argument(int index) const;
 
@@ -218,8 +219,8 @@ public slots:
     QJSValue move();
 
     QJSValue read();
-    void write();
-    void change();
+    QJSValue write();
+    QJSValue change();
     void separator();
 
     void action();
@@ -400,8 +401,19 @@ private:
     void processUncaughtException(const QString &cmd);
     void showExceptionMessage(const QString &message);
     QVector<int> getRows() const;
+
+    /**
+     * Parses arguments as one of these or raises an argument error:
+     * - item...
+     * - mimeType, data, [mimeType, data]...
+     * - list of items
+     * - text
+     */
+    QVector<QVariantMap> getItemArguments(int begin, int end, QString *error);
+    QVector<QVariantMap> getItemList(int begin, int end, const QJSValue &arguments);
+
     QJSValue copy(ClipboardMode mode);
-    void changeItem(bool create);
+    QJSValue changeItem(bool create);
     void nextToClipboard(int where);
     QJSValue screenshot(bool select);
     QByteArray serialize(const QJSValue &value);
diff --git a/src/scriptable/scriptableproxy.cpp b/src/scriptable/scriptableproxy.cpp
index 4ceec39763790fd93c262b77fa30b4254583542e..d3a6fcc5201d4d08ce6f93ac64b0255bb913be78 100644
--- a/src/scriptable/scriptableproxy.cpp
+++ b/src/scriptable/scriptableproxy.cpp
@@ -1496,36 +1496,42 @@ QString ScriptableProxy::browserInsert(const QString &tabName, int row, const QV
 
     ClipboardBrowser *c = fetchBrowser(tabName);
     if (!c)
-        return "Invalid tab";
+        return QLatin1String("Invalid tab");
 
     if ( !c->allocateSpaceForNewItems(items.size()) )
-        return "Tab is full (cannot remove any items)";
+        return QLatin1String("Tab is full (cannot remove any items)");
 
     for (const auto &item : items) {
         if ( !c->add(item, row) )
-            return "Failed to new add items";
+            return QLatin1String("Failed to new add items");
     }
 
     return QString();
 }
 
-bool ScriptableProxy::browserChange(const QString &tabName, const QVariantMap &data, int row)
+QString ScriptableProxy::browserChange(const QString &tabName, int row, const QVector<QVariantMap> &items)
 {
-    INVOKE(browserChange, (tabName, data, row));
+    INVOKE(browserChange, (tabName, row, items));
+
     ClipboardBrowser *c = fetchBrowser(tabName);
     if (!c)
-        return false;
+        return QLatin1String("Invalid tab");
 
-    const auto index = c->index(row);
-    QVariantMap itemData = c->model()->data(index, contentType::data).toMap();
-    for (auto it = data.constBegin(); it != data.constEnd(); ++it) {
-        if ( it.value().isValid() )
-            itemData.insert( it.key(), it.value() );
-        else
-            itemData.remove( it.key() );
+    int currentRow = row;
+    for (const auto &data : items) {
+        const auto index = c->index(currentRow);
+        QVariantMap itemData = c->model()->data(index, contentType::data).toMap();
+        for (auto it = data.constBegin(); it != data.constEnd(); ++it) {
+            if ( it.value().isValid() )
+                itemData.insert( it.key(), it.value() );
+            else
+                itemData.remove( it.key() );
+        }
+        c->model()->setData(index, itemData, contentType::data);
+        ++currentRow;
     }
 
-    return c->model()->setData(index, itemData, contentType::data);
+    return QString();
 }
 
 QByteArray ScriptableProxy::browserItemData(const QString &tabName, int arg1, const QString &arg2)
diff --git a/src/scriptable/scriptableproxy.h b/src/scriptable/scriptableproxy.h
index b57d3a4a5e2d1e77821f5e8b32d26599f022bc61..305ec800de9740d9b1b5e34a8e22c8f189486dbc 100644
--- a/src/scriptable/scriptableproxy.h
+++ b/src/scriptable/scriptableproxy.h
@@ -169,7 +169,7 @@ public slots:
     bool browserOpenEditor(const QString &tabName, const QByteArray &arg1, bool changeClipboard);
 
     QString browserInsert(const QString &tabName, int row, const QVector<QVariantMap> &items);
-    bool browserChange(const QString &tabName, const QVariantMap &data, int row);
+    QString browserChange(const QString &tabName, int row, const QVector<QVariantMap> &items);
 
     QByteArray browserItemData(const QString &tabName, int arg1, const QString &arg2);
     QVariantMap browserItemData(const QString &tabName, int arg1);
diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp
index 6ec02a371abd780635334b5ed6f24044ad147427..23ad4dc6ca1318908a616bd73d5c4737bba5bfc8 100644
--- a/src/tests/tests.cpp
+++ b/src/tests/tests.cpp
@@ -1104,6 +1104,25 @@ void Tests::commandsWriteRead()
     RUN("read" << COPYQ_MIME_PREFIX "test1" << "0", arg1.toLatin1());
     RUN("read" << COPYQ_MIME_PREFIX "test2" << "0", input);
     RUN("read" << COPYQ_MIME_PREFIX "test3" << "0", arg2.toLatin1());
+
+    RUN("write(1, {'text/plain': 'A'}, {'text/plain': 'B'})", "");
+    RUN("read(mimeText, 0, 1, 2, 3)", "\nB\nA\n");
+
+    RUN("write(0, [{'text/plain': 'C'}, {'text/plain': 'D'}])", "");
+    RUN("read(mimeText, 0, 1, 2, 3)", "D\nC\n\nB");
+
+    RUN("write(0, ['E', 'F'])", "");
+    RUN("read(mimeText, 0, 1, 2, 3)", "F\nE\nD\nC");
+
+    RUN_EXPECT_ERROR_WITH_STDERR(
+        "write(0, [{}], [{}])",
+        CommandException, "Unexpected multiple item list arguments");
+    RUN_EXPECT_ERROR_WITH_STDERR(
+        "write(0)",
+        CommandException, "Expected item arguments");
+    RUN_EXPECT_ERROR_WITH_STDERR(
+        "write(0, '1', '2', '3')",
+        CommandException, "Unexpected uneven number of mimeType/data arguments");
 }
 
 void Tests::commandChange()
@@ -1471,6 +1490,17 @@ void Tests::commandCopy()
          , "true\n" );
     WAIT_FOR_CLIPBOARD2("C", "DATA3");
     WAIT_FOR_CLIPBOARD2("D", "DATA4");
+
+    RUN( "copy({'DATA1': 1, 'DATA2': 2})", "true\n" );
+    WAIT_FOR_CLIPBOARD2("1", "DATA1");
+    WAIT_FOR_CLIPBOARD2("2", "DATA2");
+
+    RUN_EXPECT_ERROR_WITH_STDERR(
+        "copy({}, {})",
+        CommandException, "Expected single item");
+    RUN_EXPECT_ERROR_WITH_STDERR(
+        "copy([{}, {}])",
+        CommandException, "Expected single item");
 }
 
 void Tests::commandClipboard()