update()

Updates an existing object on the server using dms.XMLUpdate. After a successful update the passed model instance takes over the written state, see After the update.

ECM Model reference — for a full description of system.is_modified, system.modified_fields, system.modified_table_fields, system.has_removed_table_rows, system.requires_table_field_replacement, and how change tracking works.

If no fields have been changed and no files are provided, the server call is skipped by default. Pass force=True to send the request regardless.

If the fully populated instance is needed directly after the update, update_and_get() is the more compact alternative.

1. Signature

  • Sync

  • Async

ecm.dms.update(
    model: T,
    files: Sequence[JobRequestFile] | None = None,
    replace_files: bool = True,
    force: bool = False,
    *,
    check_mandatory: bool = True,
    replace_table_fields: bool | None = None,
    options: ECMUpdateOptions | None = None,
) -> None
await ecm.dms.update(
    model: T,
    files: Sequence[JobRequestFile] | None = None,
    replace_files: bool = True,
    force: bool = False,
    *,
    check_mandatory: bool = True,
    replace_table_fields: bool | None = None,
    options: ECMUpdateOptions | None = None,
) -> None

2. Parameters

Name Type Description

model

ECMFolderModel | ECMRegisterModel | ECMDocumentModel

The model instance to update. Must have been loaded from the server so that model.system.id is set.

files

Sequence[JobRequestFile] | None

Optional list of files for the document. Only allowed for ECMDocumentModel — raises TypeError for folder or register models. Default: None.

replace_files

bool

When True (default) and files are provided, the new files replace the existing document files (REPLACEFILES=1). When False, files are appended to the existing ones. Only relevant for ECMDocumentModel when files is provided.

force

bool

When True, the server call is always executed even when no changes are detected. Default: False.

check_mandatory

bool

When True (default), validates client-side that all mandatory=True ECMField descriptors have a value. When False, both client-side and server-side obligation checks are disabled (CHECKOBLIGATION=0). A field the query did not load is not checked, see Partially loaded models.

replace_table_fields

bool | None

Overrides the automatic control of the REPLACETABLEFIELDS=1 option for table fields. None (default) keeps the automatic behaviour (see below): the option is set only when change tracking detects that loaded rows were modified, removed, or replaced (model.system.requires_table_field_replacement). True forces the option even when no change is detected; False suppresses it even when a change is detected. Required for a table field that was not loaded and has been assigned or emptied, see Partially loaded models.

options

ECMUpdateOptions | None

Additional dms.XMLUpdate job options, e.g. ECMUpdateOptions().check_read_only(False). Options set here take precedence over the ones derived from the typed parameters above. See Job options. Default: None.

replace_table_fields=False overrides the automatic safeguard. If loaded rows were modified or removed and the option is suppressed, the server falls back to its append behaviour and the change may not be persisted as intended. Only use False if you know the consequences.

3. Return value

None

4. Automatic skip

The server call is silently skipped when all of the following conditions are true:

  • force=False (default)

  • model.system.is_modified is False — no fields have been changed

  • files is None

The model tracks field changes automatically (is_modified, modified_fields, modified_table_fields). Only the fields you have actually touched are serialised — untouched fields are left alone and never re-sent to the server.

Table fields are handled separately:

  • When a loaded row was modified, removed, or replaced (i.e. system.requires_table_field_replacement is True), the entire current row set is sent and REPLACETABLEFIELDS=1 is added automatically so the server replaces the table contents.

  • When only new rows were appended, only those new rows are sent — the server’s default APPEND mode keeps the existing rows from being duplicated.

  • Every row this model did not load into this table field counts as new, including one taken over from another object or table field; it is sent (#15523).

  • Table fields with no changes are omitted from the XML and remain untouched on the server.

  • A table field emptied by the caller is sent as an empty <TableField internal_name="…"/> together with REPLACETABLEFIELDS=1. That empty element is the replacement content the server needs in order to delete the remaining rows; see Clearing a table field.

This automatic behaviour can be overridden with the replace_table_fields parameter: True forces REPLACETABLEFIELDS=1 (even for pure appends), False suppresses it (even when a change is detected). None (default) keeps the automatic detection described above. Whenever REPLACETABLEFIELDS=1 is in effect, from the parameter, from options or automatically, every changed table field goes out with all its rows, also one that only grew; otherwise the new rows would replace the loaded ones (#15523).

5. After the update

After a successful update the model takes over what it wrote as its new loaded state (#15523). A second update() without new changes is therefore skipped instead of sending the same changes again; appended rows used to end up twice that way. Changes made to the model while the request is in flight stay pending.

For table fields, the state taken over depends on whether their rows on the server are known afterwards:

  • Known (a loaded table with appended rows, or REPLACETABLEFIELDS=1): the table field holds copies of exactly the rows written. The caller’s row objects stay untouched, also when other models hold them; a reference to a row taken before the update no longer belongs to the model afterwards. Rows the request did not carry (without any value, or not a row object) drop out.

  • Unknown: the table field is empty afterwards and counts as not loaded. This applies to rows appended to a table field that was not loaded or to a model built by hand with an id set, and to replace_table_fields=False after a change that needed a replacement.

The concurrency_timestamp stays the loaded one, as dms.XMLUpdate returns no new modification time. A further update with new changes therefore usually fails with -1048 (ECMWrongStateException): the server sees the object changed since it was loaded. To carry on, use the instance returned by update_and_get() or reload.

folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Title = "New"
folder = ecm.dms.update_and_get(folder)   # fresh instance with the current timestamp
folder.Year = 2026
ecm.dms.update(folder)

6. Clearing a table field

Removing every row of a loaded table field clears it on the server:

folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Positions.clear()     # or: del folder.Positions[:]
ecm.dms.update(folder)       # sends an empty <TableField/> plus REPLACETABLEFIELDS=1

The server only ever removes rows when it receives replacement content, so the empty element and the option belong together. Suppressing the option with replace_table_fields=False therefore leaves the existing rows in place — the server falls back to APPEND mode, where an empty table field says nothing.

A table field that was already empty when the object was loaded is not a change and stays out of the XML, so declared but unused table fields are never cleared by an unrelated update.

7. Clearing a field

Setting a loaded field to None clears it on the server. The field is emitted with field_function="NULL", so the stored value is removed rather than left unchanged:

folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Year = None       # reset a previously set field
ecm.dms.update(folder)   # the server clears the field (field_function="NULL")

Change tracking treats None as a modification only when the server-loaded value was non-None, so a clear is sent exactly once and a field that was already empty is not re-sent. On insert() a None field is simply omitted — there is nothing to clear on a new object.

8. Partially loaded models

A model from select().fields(…​), select_lol().fields(…​) or get(…​, use_result_list=True, fields=…​) only knows the requested fields. Every other field reads as None or as an empty table, exactly like an empty one. update() therefore only sends what is unambiguous (#15523):

Change to a field that was not loaded Behaviour

set to a value

is sent

set to None

ValueError: a clear cannot be told apart from the read value being written back. To clear the field, request it in fields().

table rows added with append(), extend(), insert() or +=

sent in APPEND mode, the rows on the server stay. When another table field triggers REPLACETABLEFIELDS=1 automatically, ValueError, because the append would turn into a replacement; with an explicit True (parameter or options) the appended rows replace the server rows.

table field assigned, emptied or changed otherwise

ValueError while replace_table_fields is missing. True replaces the rows on the server but also turns every other table field of the request into a replacement; False appends.

none

stays unchanged on the server

The error guards against round trips such as folder[name] = folder[name] over all fields, which would otherwise delete every value that was not loaded. A mandatory field that was not requested does not block an update: it is not sent, and the server keeps its value even with CHECKOBLIGATION=1.

folder = ecm.dms.select(InvoiceFolder).fields(InvoiceFolder.Title).where(InvoiceFolder.Title == "Old").execute()[0]
folder.Title = "New"         # loaded: written
folder.Year = 2026           # not loaded, but a value: written
ecm.dms.update(folder)       # Positions and all other fields stay unchanged

# a separate instance, see <<after-the-update>>
other = ecm.dms.select(InvoiceFolder).fields(InvoiceFolder.Title).where(InvoiceFolder.Title == "New").execute()[0]
other.Positions = [{"Article": "A1"}]
ecm.dms.update(other, replace_table_fields=True)   # replaces the positions on the server

9. Writable system fields

System fields (SystemFields enum) are stripped from the dms.XMLUpdate payload by default because the enaio server rejects almost all of them with ECMAccessDeniedException ("The specified system field cannot be changed by the user"). An allowlist WRITABLE_SYSTEM_FIELDS (in ecmind_blue_client.const) names the few that the server actually accepts:

Field Behaviour

OBJECT_FOREIGNID

String field. Insert + update. Holds the reference to the document in the external archive system; together with OBJECT_SYSTEMID it turns a document into a reference document (green arrow when system_id=0). Any string value is allowed — some archive configurations enforce a numeric column on the database side and will coerce non-numeric strings to '0'. Clearable via the field_function="NULL" attribute on the <Field> element.

OBJECT_SYSTEMID

Int field. Update only. On insert the server demands that OBJECT_FOREIGNID is also set (otherwise error -1013, "A system ID was specified but no foreign ID").

OBJECT_USERGUID

String field (owner). Insert + update. The value sent must be the target user’s username (e.g. "ROOT"), not the GUID hex. The server resolves the user and stores the GUID, which is what subsequent reads return. Sending a GUID hex directly fails with error -1019 ("The specified owner could not be determined").

Not available through <Field> but maintained as XML attributes on <Object>/<ObjectType> and controlled through other APIs (e.g. insert(), move()): OBJECT_MAIN (= maintype), OBJECT_CO (= cotype), OBJECT_ID (= object_id), SDSTA_ID/SDREG_ID/FOLDERID/REGISTERID/… (= folder_id/register_id/…). These also return ECMAccessDeniedException when sent as <Field>.

For OBJECT_SYSTEMID and OBJECT_FOREIGNID there is a typed path through the system namespace, which is preferable to reaching for the internal field name:

doc = ecm.dms.get(MyDocument, doc_id)
doc.system.foreign_id = None          # drop the external archive reference
doc.system.system_id = None
doc = ecm.dms.update_and_get(doc)     # the server rewrites OBJECT_FLAGS

The object has to come from the server, otherwise change tracking does not see the clear and the request goes out without the field. OBJECT_USERGUID deliberately has no setter, because reading returns a GUID while writing expects the user name. Change the owner with doc.system.assign_owner("ROOT"); the method refuses a GUID with ValueError.

Further writable system fields discovered later can simply be added to WRITABLE_SYSTEM_FIELDS without any model changes.

from ecmind_blue_client import WRITABLE_SYSTEM_FIELDS
# Extend programmatically at run-time (not persisted):
WRITABLE_SYSTEM_FIELDS.add("OBJECT_AVID")

10. Exceptions

ValueError

model.system.id is None (object was not loaded from the server) or check_mandatory=True and a required field is not set, or a read-only field (ECMField(read_only=…)) was changed — see ECM model. Also when a field that was not loaded is set to None, when a table field that was not loaded is assigned or emptied without a decision through replace_table_fields (or options), or when rows are appended to a table field that was not loaded while another table field triggers REPLACETABLEFIELDS=1 automatically, see Partially loaded models.

TypeError

files was provided for a folder or register model.

11. Examples

11.1. Simple field update

  • Sync

  • Async

folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Title = "Invoice 2024 (updated)"
ecm.dms.update(folder)
folder = await ecm.dms.get(InvoiceFolder, 12345)
folder.Title = "Invoice 2024 (updated)"
await ecm.dms.update(folder)

11.2. Replace document file

  • Sync

  • Async

from ecmind_blue_client.rpc import JobRequestFileFromPath

doc = ecm.dms.get(InvoiceDocument, 42)
doc.Title = "Invoice No. 42 (corrected)"
ecm.dms.update(
    doc,
    files=[JobRequestFileFromPath("/tmp/invoice_v2.pdf")],
    replace_files=True,  # replace existing files (default)
)
from ecmind_blue_client.rpc import JobRequestFileFromPath

doc = await ecm.dms.get(InvoiceDocument, 42)
doc.Title = "Invoice No. 42 (corrected)"
await ecm.dms.update(
    doc,
    files=[JobRequestFileFromPath("/tmp/invoice_v2.pdf")],
    replace_files=True,
)

11.3. Append file instead of replacing

  • Sync

  • Async

from ecmind_blue_client.rpc import JobRequestFileFromPath

doc = ecm.dms.get(InvoiceDocument, 42)
ecm.dms.update(
    doc,
    files=[JobRequestFileFromPath("/tmp/attachment.pdf")],
    replace_files=False,  # add file, keep existing ones
)
from ecmind_blue_client.rpc import JobRequestFileFromPath

doc = await ecm.dms.get(InvoiceDocument, 42)
await ecm.dms.update(
    doc,
    files=[JobRequestFileFromPath("/tmp/attachment.pdf")],
    replace_files=False,
)

11.4. Forcing an update

Useful when external logic requires resubmission even though no fields have changed:

  • Sync

  • Async

folder = ecm.dms.get(InvoiceFolder, 12345)
# no field changes — without force=True the call would be skipped
ecm.dms.update(folder, force=True)
folder = await ecm.dms.get(InvoiceFolder, 12345)
await ecm.dms.update(folder, force=True)

11.5. Disabling mandatory field validation

  • Sync

  • Async

folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Title = None  # clear a mandatory field
# check_mandatory=False: no error, obligation check also disabled server-side
ecm.dms.update(folder, check_mandatory=False)
folder = await ecm.dms.get(InvoiceFolder, 12345)
folder.Title = None
await ecm.dms.update(folder, check_mandatory=False)

11.6. Job options: migration with catalog values that do not exist yet

By default the server checks that every supplied catalog value exists in the configured catalog, and it rejects overlong strings rather than truncating them. Migrating from a legacy system trips over both:

  • Sync

  • Async

from ecmind_blue_client.ecm import ECMUpdateOptions

ecm.dms.update(
    folder,
    options=ECMUpdateOptions().check_catalogue(False).truncate_values(True),
)
from ecmind_blue_client.ecm import ECMUpdateOptions

await ecm.dms.update(
    folder,
    options=ECMUpdateOptions().check_catalogue(False).truncate_values(True),
)

check_catalogue(False) accepts values not yet present in the catalog, truncate_values(True) cuts overlong strings to the defined field length instead of rejecting the update. Use both deliberately and as narrowly as possible.

Full option list in Job options.

12. See also

  • update_and_get() — combines update() and get() in one call when the updated instance is needed

  • insert() — creates a new object

  • get() — loads a single object from the server