DynamoDB "Attribute Name Is a Reserved Keyword": Fix status, name, date, user and ttl With ExpressionAttributeNames (CLI, Python, Node.js and PartiQL)

Logeshwaran
—

DynamoDB's "Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: status" means you wrote an attribute name straight into an expression, and that name is one of DynamoDB's 573 reserved words. The data is fine and so is the attribute name; only the way you wrote it in the expression is wrong. The fix is a placeholder: write #st in the expression and map it with ExpressionAttributeNames, as in {"#st": "status"}. The same error appears for projection, key condition, filter and condition expressions. The surprise is how ordinary the reserved words are: name, status, date, data, count, user, type, value, size, timestamp and even ttl, the name most people give their expiry attribute, are all on the list, while id, email and title aren't. The list ignores case, so Status fails too. This page shows every version of the error, the fix in the AWS CLI, Python and Node.js, the dotted attribute name that fails without any error at all, PartiQL's different rule, and the two new errors people hit right after adding their first placeholder.

Ethan keeps the repair tickets for his friend Jake's phone shop in a DynamoDB table called Repairs, with the customer as user, the day as date, and attributes like status, name and ttl. Reading and writing whole items worked for weeks. The first time he wrote an update to mark a repair as done, DynamoDB refused, and every fix he tried led to a new error message. He hadn't picked a single unusual word. That was exactly the problem.

⚡ Quick Answer

• The fix: SET #st = :s with --expression-attribute-names '{"#st":"status"}'. The fix, step by step.

• Common reserved names: name, status, date, data, count, user, type, ttl, value, size, timestamp, year. The everyday list.

• Python: boto3's Key() and Attr() add placeholders for you; update expressions don't. boto3.

• Dots and hyphens: Safety.Warning without a placeholder returns nothing, with no error. The silent trap.

• New error after the fix? "not defined" or "unused in expressions" means the names map doesn't match the expression. Those two.

Using PartiQL? Wrap names in double quotes instead. PartiQL.

New to DynamoDB? It's AWS's NoSQL database: you store items in tables and read or change them with short expressions, small pieces of text such as SET status = :s. This page is about the words those expressions won't accept. Our guide to DynamoDB covers the basics.

🧭 NEW HERE? READ THESE FIRST

New to DynamoDB expressions? These five pages make the rest of this one easy:

📌 Bookmark this; the reserved-word table and the placeholder patterns are the parts you will come back for.

What "Attribute name is a reserved keyword" means

DynamoDB reads every expression with a small parser, and 573 words are reserved for that parser's own use. When one of them appears as a plain attribute name, DynamoDB stops with a ValidationException before touching any data. The message tells you which kind of expression failed and which word caused it:

Where you wrote itThe messageUsed by
UpdateExpressionInvalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: statusUpdateItem
ProjectionExpressionInvalid ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: nameGetItem, Query, Scan
KeyConditionExpressionInvalid KeyConditionExpression: Attribute name is a reserved keyword; reserved keyword: userQuery
FilterExpressionInvalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: ttlQuery, Scan
ConditionExpressionInvalid ConditionExpression: Attribute name is a reserved keyword; reserved keyword: datePutItem, UpdateItem, DeleteItem

Three things about the rule are worth knowing before you fix it:

  • It only applies inside expressions. You can store an attribute called status with PutItem and read the whole item back with GetItem without any trouble. The error only starts when you name it in an expression.
  • It ignores case. The list isn't case-sensitive, so Status and STATUS fail exactly like status; the message just echoes your spelling.
  • Reserved words aren't the only names that need a placeholder. So does any name that begins with a number, contains a space, or contains a dot or hyphen.

Jake: "So 'status' is fine as a column, just not when you say it out loud in an expression?"

Ethan: "Right. DynamoDB keeps a few hundred words for itself. In an expression, you refer to those by nickname."

The everyday names that are reserved, and some that aren't

Most of the 573 words are obscure SQL terms nobody uses as a field name. The trouble is the handful that everybody does. These common attribute names are all reserved:

AreaReserved words people actually use
Identity and peoplename, user, owner, role, group, session, comment
State and typestatus, state, type, level, action, source, result
Dates and timedate, time, timestamp, year, hour, zone, ttl
Values and amountsvalue, count, total, size, percent, data, items
Places and pathslocation, region, domain, url, path
Structurekey, hash, range, table, view, order, set, usage, storage, connection, return

Just as useful: id, email, title and expires are not reserved. That's why the same kind of code works on one table and fails on the next: a table keyed on id never trips the rule, and one keyed on user fails on its very first query.

You don't need to memorize the list. The habit in the next section, a placeholder for every name, makes the list irrelevant.

Check your own table's attribute names

To see which names in an existing table need a placeholder, this script reads one item and asks DynamoDB about each attribute name in turn, with a tiny projection read per name. It also flags dotted names, which never raise an error. Run it as python3 check_names.py Repairs:

import sys
import boto3
from botocore.exceptions import ClientError

TABLE = sys.argv[1]
client = boto3.client("dynamodb")

key_names = [k["AttributeName"] for k in client.describe_table(TableName=TABLE)["Table"]["KeySchema"]]
item = client.scan(TableName=TABLE, Limit=1)["Items"][0]
key = {k: item[k] for k in key_names}

for name in sorted(item):
    if "." in name:
        print(f"needs #  {name}  (contains a dot: read as a nested path, fails silently)")
        continue
    try:
        client.get_item(TableName=TABLE, Key=key, ProjectionExpression=name)
        print(f"ok       {name}")
    except ClientError as e:
        print(f"needs #  {name}  ({e.response['Error']['Message']})")
needs #  Safety.Warning  (contains a dot: read as a nested path, fails silently)
needs #  date  (Invalid ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: date)
needs #  date-collected  (Invalid ProjectionExpression: Syntax error; token: "-", near: "date-collected")
ok       details
needs #  first-name  (Invalid ProjectionExpression: Syntax error; token: "-", near: "first-name")
needs #  name  (Invalid ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: name)
needs #  status  (Invalid ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: status)
needs #  ttl  (Invalid ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: ttl)
needs #  user  (Invalid ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: user)

Eight of Ethan's nine attribute names needed a placeholder. It only checks the names in that one item, so run it against a typical item, and try it against DynamoDB Local first if you'd rather not touch a live table.

The fix: ExpressionAttributeNames

An expression attribute name is a nickname that starts with #, followed by letters, digits or underscores. You write the nickname in the expression and give DynamoDB the real name separately. The fix always takes the same five steps:

  1. Read the word at the end of the error message, for example status.
  2. Pick a placeholder for it that starts with #, such as #st.
  3. Replace the word with the placeholder everywhere it appears in your expressions.
  4. Add ExpressionAttributeNames mapping the placeholder to the real name: {"#st": "status"}.
  5. Leave your values where they were, in ExpressionAttributeValues with their : placeholders.

Here's the update that failed:

aws dynamodb update-item --table-name Repairs \
  --key '{"user":{"S":"jake"},"date":{"S":"2026-10-13"}}' \
  --update-expression "SET status = :s" \
  --expression-attribute-values '{":s":{"S":"done"}}'
An error occurred (ValidationException) when calling the UpdateItem operation: Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: status

And the same update with a placeholder:

aws dynamodb update-item --table-name Repairs \
  --key '{"user":{"S":"jake"},"date":{"S":"2026-10-13"}}' \
  --update-expression "SET #st = :s" \
  --expression-attribute-names '{"#st":"status"}' \
  --expression-attribute-values '{":s":{"S":"done"}}' \
  --return-values UPDATED_NEW
{
    "Attributes": {
        "status": {
            "S": "done"
        }
    }
}

Notice the two kinds of placeholder side by side: #st stands for an attribute name and goes in --expression-attribute-names; :s stands for a value and goes in --expression-attribute-values. Mixing them up is the most common typo in this whole area.

Keys can be reserved too

Ethan's table is keyed on user and date, both reserved, so even a plain query needs placeholders. One names map serves the key condition and the projection together:

aws dynamodb query --table-name Repairs \
  --key-condition-expression "#u = :u AND begins_with(#d, :m)" \
  --projection-expression "#n, #st, #d" \
  --expression-attribute-names '{"#u":"user","#d":"date","#n":"name","#st":"status"}' \
  --expression-attribute-values '{":u":{"S":"jake"},":m":{"S":"2026-10"}}'

The Key parameter of GetItem, UpdateItem and DeleteItem is plain JSON, not an expression, so {"user": ...} works there without any placeholder.

Jake: "So the hash sign means 'this is a name, and I'll tell you the real one in a minute.'"

Ethan: "That's it. And the colon means 'this is a value, coming up.'"

Python (boto3): what the SDK does for you, and what it doesn't

boto3 has two layers, and they behave differently here.

With the low-level client, you write every expression yourself, so you add ExpressionAttributeNames yourself. With the Table resource, the Key() and Attr() condition builders add placeholders automatically, even for reserved names like user and status. Update expressions stay plain strings in both layers, so those always need names:

import boto3
from boto3.dynamodb.conditions import Attr, Key

client = boto3.client("dynamodb")
key = {"user": {"S": "jake"}, "date": {"S": "2026-10-13"}}

# Low-level client: alias every reserved name yourself
resp = client.update_item(
    TableName="Repairs",
    Key=key,
    UpdateExpression="SET #st = :s, #ttl = :t",
    ExpressionAttributeNames={"#st": "status", "#ttl": "ttl"},
    ExpressionAttributeValues={":s": {"S": "collected"}, ":t": {"N": "1792500000"}},
    ReturnValues="UPDATED_NEW",
)
print("client update:", resp["Attributes"])

# Resource layer: Key() and Attr() add the placeholders for you
table = boto3.resource("dynamodb").Table("Repairs")
items = table.query(
    KeyConditionExpression=Key("user").eq("jake") & Key("date").begins_with("2026-10"),
    FilterExpression=Attr("status").eq("collected"),
)["Items"]
print("resource query:", [(i["user"], i["date"], i["status"]) for i in items])
client update: {'ttl': {'N': '1792500000'}, 'status': {'S': 'collected'}}
resource query: [('jake', '2026-10-13', 'collected')]

Build update expressions with a placeholder for every field

When the fields to update come from a form or an API request, don't decide name by name whether a placeholder is needed. Alias all of them. This helper turns a dictionary into a ready update:

import boto3


def build_update(fields):
    """Turn {"status": "done", "name": "Screen"} into an UpdateExpression with every name aliased."""
    names = {f"#k{i}": name for i, name in enumerate(fields)}
    values = {f":v{i}": value for i, value in enumerate(fields.values())}
    expression = "SET " + ", ".join(f"#k{i} = :v{i}" for i in range(len(fields)))
    return expression, names, values


table = boto3.resource("dynamodb").Table("Repairs")
expr, names, values = build_update({"status": "ready", "name": "Screen repair", "date-collected": "2026-10-14"})
print(expr)
resp = table.update_item(
    Key={"user": "jake", "date": "2026-10-13"},
    UpdateExpression=expr,
    ExpressionAttributeNames=names,
    ExpressionAttributeValues=values,
    ReturnValues="UPDATED_NEW",
)
print(resp["Attributes"])
SET #k0 = :v0, #k1 = :v1, #k2 = :v2
{'date-collected': '2026-10-14', 'name': 'Screen repair', 'status': 'ready'}

It handled a reserved word, another reserved word and a hyphenated name without a single special case. Our PutItem vs UpdateItem guide builds on the same idea for upserts and update-only writes. If a ValidationException still slips through, our boto3 error handling guide shows how to read the error code and message from ClientError.

Node.js (AWS SDK v3)

The Document Client in AWS SDK for JavaScript v3 doesn't add placeholders for you, so you pass ExpressionAttributeNames just as in the CLI. This update also changes a nested attribute, details.OneStar, with one placeholder per level of the path:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, UpdateCommand } from "@aws-sdk/lib-dynamodb";

const doc = DynamoDBDocumentClient.from(new DynamoDBClient({}));

const result = await doc.send(
  new UpdateCommand({
    TableName: "Repairs",
    Key: { user: "jake", date: "2026-10-13" },
    UpdateExpression: "SET #st = :s, #d.#one = :n",
    ExpressionAttributeNames: { "#st": "status", "#d": "details", "#one": "OneStar" },
    ExpressionAttributeValues: { ":s": "picked-up", ":n": 3 },
    ReturnValues: "UPDATED_NEW",
  })
);
console.log(JSON.stringify(result.Attributes));
{"details":{"OneStar":3},"status":"picked-up"}

Save it as update.mjs and run it with node update.mjs after npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb. Note #d.#one: a placeholder stands for one attribute name, never a whole path, so a nested path needs one for each part.

The silent trap: dots, hyphens and names starting with a number

Reserved words fail loudly. Dotted names can fail silently, the same kind of quiet empty result our scan filter guide chases in filters. In an expression, a dot means "look inside a map," so DynamoDB reads Safety.Warning as the Warning field inside a map called Safety. If your attribute is really one flat name with a dot in it, DynamoDB finds nothing and says nothing:

aws dynamodb get-item --table-name Repairs \
  --key '{"user":{"S":"jake"},"date":{"S":"2026-10-13"}}' \
  --projection-expression "Safety.Warning"
{
    "Item": {}
}

No error, and an empty item, even though the attribute exists. With a placeholder, DynamoDB treats the whole thing as one name:

aws dynamodb get-item --table-name Repairs \
  --key '{"user":{"S":"jake"},"date":{"S":"2026-10-13"}}' \
  --projection-expression "#sw" \
  --expression-attribute-names '{"#sw":"Safety.Warning"}'
{
    "Item": {
        "Safety.Warning": {
            "S": "Battery swelling"
        }
    }
}

Hyphens fail loudly instead, with a syntax error that never mentions reserved words, which sends people looking in the wrong place. If your message is "One or more parameter values were invalid" instead, that's a different problem with the item itself, covered in our invalid parameter values guide:

Invalid ProjectionExpression: Syntax error; token: "-", near: "first-name"

The fix is the same placeholder, #fn mapped to first-name. Names that begin with a number, such as 2fa, and names with spaces need one too.

Jake: "So the dot one is worse, because it doesn't even complain."

Ethan: "Much worse. An error gets fixed the same day. An empty answer can sit in a report for weeks."

Two new errors right after you add a placeholder

Once you start using placeholders, two new messages appear, both about the names map not matching the expression. When either shows up, check in this order:

  1. Every # placeholder in every expression appears in ExpressionAttributeNames.
  2. Every key in ExpressionAttributeNames is used by at least one expression in the same request.
  3. Names start with # and values with :, and each sits in its own map.
  4. The spelling of each placeholder matches exactly on both sides.

"An expression attribute name used in the document path is not defined"

Invalid UpdateExpression: An expression attribute name used in the document path is not defined; attribute name: #s

The expression uses #s, but ExpressionAttributeNames doesn't define it, or isn't being sent at all. Check the spelling on both sides: #st in the expression and #s in the map are different names.

"Value provided in ExpressionAttributeNames unused in expressions"

Value provided in ExpressionAttributeNames unused in expressions: keys: {#n}

The opposite: you defined #n, but no expression in the request uses it. DynamoDB refuses requests with unused placeholders. It's common in code that builds one big names map and reuses it for requests that only touch some of the fields. Build the map from the fields actually used in each request, as the helper above does.

PartiQL: double quotes instead of placeholders

DynamoDB's SQL-like PartiQL statements don't use ExpressionAttributeNames. Instead, wrap attribute and table names in double quotes, and string values in single quotes. Unquoted, a keyword like user breaks the statement:

aws dynamodb execute-statement --statement "SELECT * FROM Repairs WHERE user = 'jake'"
An error occurred (ValidationException) when calling the ExecuteStatement operation: Statement wasn't well formed, can't be processed: SELECT * FROM Repairs WHERE user = 'jake'

Quoted, it works:

aws dynamodb execute-statement --statement "SELECT \"status\" FROM \"Repairs\" WHERE \"user\" = 'jake'"

PartiQL's own keywords don't line up exactly with the expression list: in the same statement, an unquoted status in the SELECT list passed, while an unquoted user in the WHERE clause didn't. Rather than learning a second list, quote every attribute name in PartiQL, just as you alias every name in expressions.

Should you rename the attribute instead?

Usually not. DynamoDB has no rename: changing status to ticket_status means rewriting every item in the table, updating every reader and writer, and handling both names during the switch. A placeholder costs one line. Renaming makes sense only in a brand-new table, before any data exists, and even then the "alias every name" habit protects you from the hundreds of reserved words you haven't thought of.

For a new table, simple choices avoid the issue: id instead of user for a key, ticketStatus instead of status, expiresAt instead of ttl for the time-to-live attribute.

Troubleshooting by symptom

Invalid UpdateExpression: Attribute name is a reserved keyword

Replace the name in the expression with a placeholder such as #st and add ExpressionAttributeNames {"#st": "status"}.

The same update worked yesterday on another table

That table's attribute names probably aren't reserved, such as id instead of user. Alias every name and both tables work the same.

Query fails on the key attribute itself

Your key name is reserved, like user or date. Use placeholders in KeyConditionExpression; the Key parameter of GetItem and UpdateItem doesn't need them.

Projection returns an empty item with no error

The attribute name has a dot, which DynamoDB read as a nested path. Use a placeholder for the whole name.

Syntax error; token: "-"

The attribute name has a hyphen. Put it behind a placeholder, such as #fn for first-name.

"Value provided in ExpressionAttributeNames unused in expressions"

A placeholder in the names map isn't used by any expression. Remove it, or build the map from the fields you actually use.

PartiQL statement wasn't well formed

An unquoted keyword like user broke the parser. Put attribute and table names in double quotes, and values in single quotes.

DynamoDB reserved keywords: frequently asked questions

What does "Attribute name is a reserved keyword" mean in DynamoDB?

You used one of DynamoDB's 573 reserved words, such as status, name or date, directly in an expression. Replace it with a placeholder like #st and map it with ExpressionAttributeNames.

How do I fix "Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: status"?

Write SET #st = :s and add ExpressionAttributeNames {"#st": "status"} along with your ExpressionAttributeValues. The same pattern works for any reserved word.

Is "name" a reserved word in DynamoDB?

Yes. So are status, date, data, count, user, type, value, size, timestamp and ttl. You can store attributes with those names, but expressions must refer to them through a placeholder.

Is "status" a reserved word in DynamoDB?

Yes. Use #st (or any # name) in the expression and map it to status in ExpressionAttributeNames.

Is "ttl" a reserved word in DynamoDB?

Yes. Many tables use ttl for their time-to-live attribute, and it works for TTL itself, but any expression that mentions it needs a placeholder.

Are DynamoDB reserved words case-sensitive?

No. The list ignores case, so Status and STATUS are as reserved as status.

What is ExpressionAttributeNames in DynamoDB?

A map of placeholders, each starting with #, to real attribute names. You use it for reserved words, names with dots, hyphens or spaces, names starting with a number, and nested paths.

What is the difference between ExpressionAttributeNames and ExpressionAttributeValues?

Names (#st) stand for attribute names; values (:s) stand for the data you compare or write. A typical update uses both: SET #st = :s.

Does boto3 handle DynamoDB reserved words automatically?

Partly. The Table resource's Key() and Attr() conditions add placeholders for you. Update expressions and the low-level client don't, so pass ExpressionAttributeNames there.

How do I use reserved words in a DynamoDB query KeyConditionExpression?

Use placeholders for the key names, for example #u = :u with ExpressionAttributeNames {"#u": "user"}.

How do I use a reserved word in a FilterExpression?

Exactly the same way: write #st = :s in the filter and map #st to the real name. The names map is shared by all expressions in the request.

How do I reference a nested attribute in DynamoDB?

Use one placeholder per level, such as #d.#one with {"#d": "details", "#one": "OneStar"}. A single placeholder can't stand for a whole path.

Why does my DynamoDB projection return nothing for an attribute with a dot?

DynamoDB reads the dot as a path into a map. If your attribute's name contains a dot, use a placeholder for the whole name so it's read as one attribute.

What does "Value provided in ExpressionAttributeNames unused in expressions" mean?

You defined a placeholder that no expression in the request uses. Remove it from ExpressionAttributeNames, or only add placeholders for fields you actually use.

How do I use reserved words in DynamoDB PartiQL?

Wrap attribute and table names in double quotes, for example SELECT "status" FROM "Repairs" WHERE "user" = 'jake'. String values stay in single quotes.

How many reserved words does DynamoDB have?

573, mostly SQL terms. The ones that cause trouble are ordinary names like name, status, date, data, count, user, type, value and ttl.

Should I rename attributes that are DynamoDB reserved words?

Usually not. DynamoDB has no rename, so it means rewriting every item. A placeholder is one line; save new names for new tables.

For readers preparing for an AWS certification

We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.

Exams that test this topic: Developer – Associate (DVA-C03 from December 1, 2026).

Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:

1. Why does an expression using the attribute name or status fail?

They are DynamoDB reserved words. Use a placeholder such as #n in ExpressionAttributeNames.

2. Which other names need placeholders?

Names with special characters such as dots or hyphens, and nested paths that would otherwise be misread.

3. Do placeholders cost extra capacity?

No. They only change how the expression is parsed.

4. What prefix do value placeholders use?

A colon, such as :status, defined in ExpressionAttributeValues.

5. How do you avoid reserved words entirely?

Choose attribute names that are not on the list, or always use placeholders in generated code.

6. Which expressions accept these placeholders?

Key condition, filter, projection, condition and update expressions.

Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.

Ethan kept the attribute names. He changed one habit instead: every expression in the shop's code now goes through a small helper that aliases every name, and the next three reserved words he would have hit, ttl, name and user, never caused a single error. The dotted Safety.Warning field that had been quietly missing from the weekly report came back the same afternoon.

📌 If you keep one line from this page

In DynamoDB expressions, give every attribute name a # placeholder and map it in ExpressionAttributeNames; reserved words, dots and hyphens then stop mattering.

Names = #name · values = :value · nested = #a.#b · PartiQL = "double quotes".

Revision note. Written October 13, 2026, for everyone whose first update hit a word as plain as status. "Call a spade a spade," the saying goes; in DynamoDB, call it #spade and say which spade you mean.

Related