dynobase-icon
Dynobase

DynamoDB update expressions: SET, REMOVE, ADD and DELETE

Rafal Wilinski

Written by Rafal Wilinski

Published: October 23rd, 2026

Updating many items at once? Dynobase applies attribute changes to selected items or matching results, shows the scope before writing, and keeps item history with undo. Bulk editing in Dynobase

A DynamoDB UpdateExpression changes attributes on one item identified by its complete primary key. Use SET to assign values, REMOVE to delete attributes, ADD to increment numbers or merge sets, and DELETE to subtract set members. You can combine all four clauses in one atomic UpdateItem request, with each clause keyword appearing at most once. If the item does not exist, the update can create it; add a ConditionExpression when creation or an unexpected overwrite must be rejected.

The examples below ran against DynamoDB Local on port 8000. Their outputs show actual item contents, including failed updates that left the item unchanged. Local tests verify expression behavior; billing and service limits are checked against AWS documentation.

DynamoDB update expression syntax

ClauseWhat it changesExample shape
SETAny supported attribute type; numeric arithmetic; list and map pathsSET #status = :status, #stock = #stock + :delta
REMOVEWhole attributes, map members, list elementsREMOVE #obsolete, #items[1]
ADDNumbers or sets at the top levelADD #stock :delta, #tags :newTags
DELETEMembers of a top-level setDELETE #tags :oldTags

Separate actions inside a clause with commas. Separate different clauses with spaces. Clause order in the string is flexible. SET a = :a SET b = :b is invalid; use one SET containing both assignments. DELETE here removes set members; deleting an entire item uses the separate DeleteItem API.

An UpdateExpression combines four clauses, with name aliases resolving to item attributes and value tokens resolving to typed inputs.

A name beginning with # belongs in ExpressionAttributeNames. A value beginning with : belongs in ExpressionAttributeValues. They are separate maps, and their entries must match the tokens actually used in the update or condition. Alias reserved words such as status, and names containing punctuation. Alias each segment of a nested path separately: #profile.#city. Mapping one alias to profile.city addresses a literal attribute with a dot in its name.

The AWS update-expression reference defines the grammar. Unlike PutItem, an update preserves attributes it does not target. Replacing a whole map with SET, however, replaces that map's contents, including members omitted from the new value.

Run the examples locally

Use Node.js 20 or later, a disposable directory and a fresh local container. These commands install dependencies there, with no AWS account required:

Save this shared fixture as setup.mjs. It creates UpdateExpressionDemo, whose only key is the String attribute pk. On a table with a sort key, supply both key attributes in every request.

Every JavaScript example imports that file. Save each block under its stated filename and run node filename.mjs. The fixture replaces item-1 before each case, then reads it before and after the update. Those reads are test instrumentation; an application can normally use the update's ReturnValues without extra reads.

Output keys are sorted for reproducibility. A displayed {"$set":[...]} represents an SDK Set, with sorted members for display only; DynamoDB sets have no order. before: null means the item was absent. This display convention is not a DynamoDB storage format.

Combine SET, REMOVE, ADD and DELETE in one update

Save as clauses.mjs. This publishes an item, removes a temporary flag, adds stock and removes an old tag in one request:

One atomic update changes status from draft to live, removes obsolete, increments stock from 5 to 7 and removes old from the tags set.

The four changes succeed together. A malformed action or a failed condition prevents the whole update.

SET: assignments, arithmetic and if_not_exists

SET overwrites an existing value or creates an attribute. Arithmetic supports addition and subtraction on Numbers. if_not_exists(path, fallback) supplies a default when that attribute is absent; an existing NULL attribute still exists and does not select the fallback.

Save as set.mjs. Both cases subtract stock, but only the first writes created:

if_not_exists is a function inside SET, not a substitute for an update condition. Other actions in the expression still run when the attribute exists. Function names are case-sensitive.

Append or prepend a DynamoDB list

list_append(left, right) concatenates two Lists. Put the existing list on the left to append, or on the right to prepend. An Array in the JavaScript document client is a List; new Set(...) is a different DynamoDB data type.

Save as lists.mjs. The first loop handles both an absent list and an existing list. The last case replaces index zero and writes beyond the list's end:

Indexes start at zero. Writing a nonexistent index appends the element; it does not create empty slots up to that index. Use list_append when appending is the intent. Concurrent position-based edits can target a different logical element after another writer changes the list; protect such edits with a version condition when identity matters.

Update a nested map attribute

Save as nested.mjs:

The sibling tier survives. The parent profile must already be a Map; DynamoDB does not construct intermediate maps for profile.city. if_not_exists(profile.city, :city) cannot repair a missing parent. The troubleshooting section shows the error and a separate initialization step.

REMOVE: delete attributes and list elements

REMOVE takes paths without values. Removing a missing top-level attribute is a no-op. Save as remove.mjs:

Both list indexes refer to the original list: book and bag disappear, then the remaining elements shift together. No need to reverse the indexes within one expression. Across separate requests, indexes can shift between calls. Removing a nested member also requires a valid parent path.

ADD: increment a counter or add to a set

A Number operand increments the existing Number. A negative operand decrements it. If the attribute is absent, DynamoDB starts from zero. A Set operand unions its members with the stored set, without duplicates; an absent set is created. Both sets must have the same member type: String, Number or Binary.

Save as add.mjs. The final case uses SET arithmetic for a nested counter because ADD only accepts top-level paths:

The counter operation is atomic, so it avoids a read-then-write lost increment. It is not idempotent: retrying a successful increment increments again. The same applies to SET #n = #n + :n and to list appends. A clause named SET does not itself make an operation safe to replay. Use a conditional version or an application idempotency design when a timeout leaves success uncertain. See atomic counter troubleshooting for missing attributes and type mismatches.

DELETE: remove members from a set

Save as delete.mjs:

The first case keeps sale; the second removes the tags attribute because no members remain. You cannot pass an empty set as the operand. Use REMOVE #tags to remove the whole attribute, and a List plus SET/REMOVE when order or duplicate entries matter.

How are multiple actions evaluated?

Right-hand attribute references use the item's values before the update, even if another action changes or removes the referenced attribute. The expression is not a sequence of JavaScript assignments.

Save as evaluation.mjs:

previousStock becomes 5, not 7. Referencing a path is allowed; assigning overlapping target paths is not. For example, trying to set both profile and profile.city in one expression is rejected. Reordering clauses cannot resolve that overlap.

UpdateItem upserts unless a condition prevents it

Save as upsert.mjs. Both cases start with no item. The second adds an existence condition:

The first creates the key and the assigned attributes. The second fails without creating anything. An upsert still needs a valid expression: arithmetic on an absent Number without a default, or an update through a missing map, can fail.

Conditional updates and optimistic locking

A condition is tested atomically with the write. The conditional update guide covers existence and value checks. For optimistic locking, compare the version you read and increment it in the same update.

Save as conditional.mjs. These independent cases start at version 3; one caller expects that version and the other expects an older version:

The failed request leaves stock and version unchanged. ReturnValuesOnConditionCheckFailure: "ALL_OLD" returns the item that failed the condition in the exception's Item field. In the tested JavaScript SDK, that exception field is low-level DynamoDB JSON, so the fixture calls unmarshall(error.Item). It can be absent when the item does not exist. Handle the conflict in application logic rather than blindly repeating the same condition.

Keeping historical versions is a separate requirement from rejecting stale writes; see DynamoDB data versioning for that model.

ReturnValues: what does UpdateItem return?

ReturnValues controls a successful response's Attributes field. Returned attributes are strongly consistent and do not consume read capacity. ReturnValuesOnConditionCheckFailure is separate and accepts only ALL_OLD or NONE. These behaviors are specified in the UpdateItem API reference.

ReturnValuesReturned attributes
NONE (default)No Attributes field
ALL_OLDEntire item before the update
UPDATED_OLDPrevious values of affected attributes
ALL_NEWEntire item after the update
UPDATED_NEWNew values of affected attributes that remain present

Save as returns.mjs. Every iteration resets the same starting item:

The fixture prints returned: null for an absent Attributes field. Notice that UPDATED_OLD includes the removed obsolete attribute, while UPDATED_NEW omits it and the untouched stock attribute. Neither option is a general-purpose record of deletions and additions; use both images in your own audit workflow if needed.

SDK v3 document client versus low-level DynamoDB JSON

Use UpdateCommand from @aws-sdk/lib-dynamodb with ordinary JavaScript values, or UpdateItemCommand from @aws-sdk/client-dynamodb with explicit type descriptors. Do not feed { N: "2" } to a document-client numeric operand: it represents a Map there. The AWS JavaScript programming guide explains the two layers and marshall/unmarshall.

Save as low-level.mjs:

Numbers on the low-level wire are strings under N. The command still increments a Number, not a String.

AWS CLI v2 example

After running setup.mjs, save this as cli.sh and run bash cli.sh. The first printed item is the strongly consistent read before the update; the second is ALL_NEW afterwards. Every call targets the local endpoint with dummy credentials.

Build an UpdateExpression dynamically from a partial object

A patch builder needs explicit meanings for omission, null and removal. These helpers treat each object key as a literal top-level attribute name. They alias every name, generate one clause per action type, reject primary-key changes, and omit ExpressionAttributeValues for remove-only patches.

Patch inputMeaning
Missing property; JavaScript undefinedLeave it unchanged
JavaScript null; Python NoneStore DynamoDB NULL
REMOVE sentinelDelete the attribute
Map/object or List/ArrayReplace that whole value
Name containing .Literal name, not a nested path

These are expression builders, not schema validators. Validate types, permitted fields and expression size at your application's boundary. Pass both key names if your table has a sort key. For a nested patch, build separate aliases for explicit path segments and reject ancestor/descendant target collisions; do not split arbitrary user-supplied names on dots.

TypeScript builder

Save as builder.ts:

Save this as builder-demo.ts and run npx tsx builder-demo.ts:

The output demonstrates the literal audit.label name, stored null, an omitted undefined value, a remove-only update, and rejection of an empty patch and a primary-key change. To modify a sibling inside a Map, use a nested path as shown earlier; passing a partial Map to this helper replaces the existing Map.

Python builder with boto3 Table.update_item

Save as builder.py and run venv/bin/python builder.py after creating the table with setup.mjs. This uses boto3's resource Table.update_item, so values are native Python values. Use Decimal for non-integer numbers instead of Python float; the JSON formatter below renders non-integral Decimals as strings to avoid display precision loss.

Python has no JavaScript-style undefined; omit a dictionary key to leave the attribute unchanged. None writes NULL, and only the sentinel requests removal.

Common UpdateExpression errors and their fixes

The following messages were reproduced from DynamoDB Local. Each failing request below started and ended with exactly {"pk":"item-1"}; no requested change was applied. The ValidationException prefix is the exception name. SDK wrappers may format their surrounding message differently.

Two document paths overlap

The failing expression was SET #p = :p, #p.#c = :c, with #p mapped to profile and #c to city. Set the whole Map once, or update only its children when the Map already exists. Parent initialization and child updates must be separate requests.

The document path is invalid for update

SET #p.#c = :c failed because profile was absent. Initialize the Map separately as shown below. An existing parent of the wrong type also needs correction.

Invalid UpdateExpression: Syntax error

The incomplete expression SET #s = has no operand. Supply a value token and its mapping, as in the upsert example. Also check commas and parentheses.

Attribute name is a reserved keyword

The failing expression used SET status = :s. Change it to SET #s = :s with ExpressionAttributeNames mapping #s to status.

The SET section can only be used once

SET #s = :s SET #n = :n repeats the clause keyword. Use SET #s = :s, #n = :n instead.

An expression attribute value is not defined

SET #s = :missing used an unmapped token. Add :missing to ExpressionAttributeValues, or correct the expression to use the token you actually supplied.

The expression refers to an attribute that does not exist

SET #l = list_append(#l, :new) failed with no items attribute. Use if_not_exists(#l, :empty) with an empty List, as in lists.mjs. An existing non-List value needs a type correction instead.

Cannot update a primary-key attribute

SET #pk = :pk tried to change pk. The key belongs in Key, not among the update targets. Moving an item to another key requires creating the new item and deleting the old one, with a transaction if they must succeed together.

An empty set is invalid

This exact message came from a low-level ADD #t :empty request with :empty set to SS: []. An empty JavaScript Set can instead fail earlier during marshalling. Omit an empty union/subtraction operation, or use REMOVE to delete the entire stored Set. Do not convert an empty Set to NULL unless that type change is intended.

Fix a missing parent map

Create the parent as part of the original item whenever possible. For existing data with an optional parent, initialize it with if_not_exists in one request, then update the child in another. Save as fix-parent.mjs; the fixture shows the state transition for each request:

Those are two operations, not one atomic unit. A concurrent writer can change or remove the map between them; add an appropriate condition and handle failure, or enforce the map's presence in your item schema. Trying to combine parent initialization and child assignment in one expression still produces the overlap error. If replacing the whole Map is intended, assign the complete Map once.

How update writes are billed

Updating one small attribute does not mean paying only for that attribute. DynamoDB uses the larger item size before or after an UpdateItem, rounded up in 1 KB units. A change from a 2.2 KB item to a 2.3 KB item therefore uses three ordinary write units; deleting attributes from 2.3 KB down to 0.8 KB still uses three for that operation. A transactional write uses twice the ordinary units. Index maintenance can add write consumption.

Failed conditions can also consume write capacity. Returning attributes does not add a read-capacity charge. AWS documents this in read and write operation accounting. Use ReturnConsumedCapacity: "INDEXES" to observe table and index consumption on service requests; DynamoDB Local is not a production billing benchmark.

Expression and item limits

ConstraintLimit
Each expression string4 KB
Each substitution token, such as #name or :value255 bytes
Total expression substitution variables2 MB
Operators and functions per expression300
Document-path dereferences32
Item size after the update400 KB, including names and values

These are fixed constraints in the AWS constraints reference and document-path rules. The token limit does not cap a stored String value at 255 bytes. The operator limit covers condition, filter and update expressions; assignment = inside SET does not count, while arithmetic, functions and condition comparisons do. A generated patch must fit all applicable limits.

Can DynamoDB update many items with one expression?

An UpdateExpression addresses one item. DynamoDB has no arbitrary multi-item UPDATE ... WHERE operation. Even PartiQL UPDATE must resolve to one primary key.

NeedApproach
Change independent known keysIssue individual UpdateItem requests with bounded parallelism and per-item error handling
Find matching items firstQuery a suitable key/index, paginate the result, then update each full key; recheck mutable predicates with a condition
Send several PartiQL updates togetherBatchExecuteStatement, up to 25 write statements; inspect each result for errors
Make several updates succeed togetherTransactWriteItems, up to 100 distinct items and 4 MB, within one account and Region

BatchWriteItem supports puts and deletes, not updates. The batch write and update guide covers those differences. Use transactions when partial completion would break an invariant; do not include two actions for the same item in one transaction. The batch statement API and transaction API specify these boundaries.

For an occasional manual correction, Dynobase bulk editing provides bulk edits, undo and item history without writing a custom patch script.

When finished with the examples, stop the disposable container with docker stop dynamodb-local; its in-memory table disappears.

Frequently Asked Questions

What is a DynamoDB UpdateExpression?

It is the expression passed to UpdateItem to change attributes on one item. It can contain SET, REMOVE, ADD and DELETE clauses, each at most once, with comma-separated actions within a clause.

How do I append to a DynamoDB list if it does not exist?

Use SET with list_append(if_not_exists(path, :empty), :new), where :empty and :new are Lists. For a nested path, its parent Map must already exist.

Can DynamoDB update a nested map without replacing it?

Yes. SET a nested path, using a separate name alias for each path segment. Sibling attributes remain unchanged, but every intermediate parent must exist with the correct type.

Does UpdateItem create an item if it does not exist?

Yes, when the expression can be evaluated on an absent item. Add a ConditionExpression checking attribute_exists on the partition-key attribute to require an existing item.

What is the difference between REMOVE and DELETE?

REMOVE deletes an attribute or a List element. DELETE subtracts members from a top-level Set. Neither clause deletes the whole item; use DeleteItem for that.

Can I update multiple items with one UpdateExpression?

No. Each UpdateItem needs one complete primary key. Use bounded parallel updates for independent changes or TransactWriteItems for changes that must succeed together; BatchWriteItem does not support updates.

Dynobase is a Professional GUI Client for DynamoDB

Start your 7-day free trial today

Product Features

Download
/
Pricing
/
Member Portal
/
Privacy
/
EULA
/
Twitter
© 2026 Dynobase