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 |
|---|---|---|
|
|
The model instance to update. Must have been loaded from the server so that |
|
|
Optional list of files for the document. Only allowed for |
|
|
When |
|
|
When |
|
|
When |
|
|
Overrides the automatic control of the |
|
|
Additional |
|
|
4. Automatic skip
The server call is silently skipped when all of the following conditions are true:
-
force=False(default) -
model.system.is_modifiedisFalse— no fields have been changed -
filesisNone
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_replacementisTrue), the entire current row set is sent andREPLACETABLEFIELDS=1is 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 withREPLACETABLEFIELDS=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=Falseafter 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 |
|
table rows added with |
sent in APPEND mode, the rows on the server stay. When another table field triggers |
table field assigned, emptied or changed otherwise |
|
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 |
|---|---|
|
String field. Insert + update. Holds the reference to the document in the external archive system; together with |
|
Int field. Update only. On insert the server demands that |
|
String field (owner). Insert + update. The value sent must be the target user’s username (e.g. |
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.idisNone(object was not loaded from the server) orcheck_mandatory=Trueand 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 toNone, when a table field that was not loaded is assigned or emptied without a decision throughreplace_table_fields(oroptions), or when rows are appended to a table field that was not loaded while another table field triggersREPLACETABLEFIELDS=1automatically, see Partially loaded models. TypeError-
fileswas 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()andget()in one call when the updated instance is needed -
insert() — creates a new object
-
get() — loads a single object from the server