diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md
index 7bc962206..823026e87 100644
--- a/reference/resources/resource-api.md
+++ b/reference/resources/resource-api.md
@@ -216,6 +216,25 @@ product.subtractFrom('quantity', 1);
product.save();
```
+#### What a tracked record writes
+
+
+
+Only the properties you assign or change are written. Reading a property, including a sub-object or an array element, is not a write: nothing is saved for it, nothing is added to the audit history, and on a [write-through caching](#write-through-caching) table nothing is sent to the source's `patch`. A sub-object or array you change is written as its whole new value.
+
+A `Date`, `Bytes` (`Buffer` / `Uint8Array`), or other value that is not a plain object or array (such as a `Map`) must be reassigned to be saved. Reading a `Date` or `Bytes` property returns a copy, so changing it in place, such as `product.when.setTime(...)` or `product.bytes[0] = 9`, is not saved. Build a new value and assign it:
+
+```javascript
+product.when = new Date(product.when.getTime() + 60_000);
+const bytes = Buffer.from(product.bytes);
+bytes[0] = 9;
+product.bytes = bytes;
+```
+
+`Date` and `Bytes` elements of an array property are the exception: changing one in place, such as `product.dates[0].setTime(...)`, saves the array.
+
+On a table that is not [`@sealed`](../database/schema.md#sealed), assigning a property the schema does not declare (`product.extra = 'x'`) saves it, even when nothing else changed.
+
---
### `publish(target: RequestTarget | Id, message: object, context?: Resource | Context): Promise`
@@ -834,22 +853,24 @@ Cookie-based sessions are intended for browser clients. For non-browser clients
## Resource Instance Methods
-A Resource instance is used to update and interact with a single record/resource. It provides functionality for updating properties, accessing property values, and managing record lifecycle. The Resource instance is normally retrieved from the static `update()` method. An instance from a table has updatable properties that can used to access and update individual properties (for properties declared in the table's schema), as well methods for more advanced updates and saving data. For example:
+A Resource instance is used to update and interact with a single record/resource. It provides functionality for updating properties, accessing property values, and managing record lifecycle. The Resource instance is normally retrieved from the static `update()` method. An instance from a table has updatable properties that can be used to access and update individual properties, as well as methods for more advanced updates and saving data. For example:
```javascript
const product = await Product.update(32);
-product.status = 'active'; // we can directly change properties on the updatable record, if they are declared in the schema
+product.status = 'active';
product.subtractFrom('quantity', 1); // We can use CRDT incrementation/decrementation to safely update the quantity
product.save();
```
+Properties declared in the schema can be assigned directly. On a table that is not `@sealed`, an undeclared property can be assigned too, and is saved. See [What a tracked record writes](#what-a-tracked-record-writes).
+
### `save()`
This saves the current state of the resource to the database in the current transaction. This method can be called after making changes to the resource to ensure that those changes have been saved to the current transaction and can be queried within the same transaction. Any pending changes are automatically saved when the transaction commits (if `save()` has not already saved them).
This method only saves data when using RocksDB storage engine, and is a no-op when using LMDB.
-### `addTo(property: string, value: number)`
+### `addTo(property: string, value: number | bigint)` {#addtoproperty-string-value-number}
Adds `value` to `property` using CRDT incrementation — safe for concurrent updates across threads and nodes.
@@ -863,13 +884,23 @@ static async post(target, data) {
// GET /MyTable/test?foo=bar → primary key is 'test?foo=bar'
```
-### `subtractFrom(property: string, value: number)`
+As of v5.4.0, calls on the same property in one transaction combine:
+
+- Two deltas add up. `record.addTo('n', 5); record.addTo('n', 3)` adds 8 as one commutative addition, which still merges with other writers' additions.
+- A delta after an assignment adds to the assigned value. `record.n = 100; record.addTo('n', 7)` stores 107. The write is still an assignment, so it replaces a concurrent writer's change instead of merging with it.
+- An assignment after `addTo` replaces the pending delta.
+
+As of v5.4.0, on a property declared `Int`, `Long`, `Float` or `Number`, a BigInt `value` is converted to a number. One beyond 2^53 in magnitude is rejected with a 400 error.
+
+### `subtractFrom(property: string, value: number | bigint)` {#subtractfromproperty-string-value-number}
+
+Subtracts `value` from `property` using CRDT incrementation. It combines with other calls in the transaction the same way as [`addTo`](#addtoproperty-string-value-number).
-Subtracts `value` from `property` using CRDT incrementation.
+As of v5.4.0, `value` can be a BigInt, like `addTo`: `record.subtractFrom('total', 35n)`. The conversion rule for number-typed properties in [`addTo`](#addtoproperty-string-value-number) applies.
### `set(property: string, value: any): void`
-Sets a property to `value`. Equivalent to direct property assignment (`record.property = value`), but can be used when the property name is dynamic and not declared in the schema.
+Sets a property to `value`. Equivalent to direct property assignment (`record.property = value`), but useful when the property name is dynamic. On a table that is not `@sealed`, the property does not need to be declared in the schema.
```javascript
const record = await Table.update(target.id);
@@ -954,6 +985,7 @@ A few limitations to design around:
- `addTo` and `subtractFrom` return no value, and a read-back immediately after a write is not guaranteed to reflect your own committed result. You cannot use them to read "your position" for an exactly-once claim or a hard limit.
- `subtractFrom` has no floor; concurrent decrements can drive a value negative.
+- Assigning a property and calling `addTo` on it in the same transaction makes the write an assignment, which does not merge with concurrent writers. See [`addTo`](#addtoproperty-string-value-number).
For non-commutative operations — claim-once, hard caps, inventory floors, state-machine transitions — serialize the operation through a single owning process, or use an external lock or coordinator. This matters more in a cluster: a counter replicates and converges eventually, but a decision made on one node's not-yet-converged view can be wrong during the replication window. A rate limiter built this way, for example, can transiently over-admit when requests are spread across nodes.
diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md
index 8216b8f45..1ed92dd7c 100644
--- a/release-notes/v5-lincoln/5.4.md
+++ b/release-notes/v5-lincoln/5.4.md
@@ -44,6 +44,20 @@ A durable MQTT session now resumes through the same checked replay. When a recon
A static `mcpResources` entry can now name a `subscribe` method, so MCP clients can watch a custom URI such as `memory:///{id}` with `resources/subscribe`. The method is called like the entry's read method, as the session's user, and returns an async iterable; each value it yields sends `notifications/resources/updated` with the URI. Harper releases the stream on unsubscribe, when the GET stream closes, on `DELETE /mcp`, and when the session's token expires or its user is dropped. A subscribe that fails now answers a JSON-RPC error instead of an HTTP 500, for row-backed resources too, and a reconnecting GET stream opens without waiting for its subscriptions to be restored. See [Subscribing to custom resources](/reference/v5/mcp/tools-and-resources#subscribing-to-custom-resources).
+## Resource API
+
+### In-Place `Date` and `Bytes` Changes No Longer Save
+
+On a record returned by `update()`, a `Date` or `Bytes` property must now be reassigned to be saved. Changing it in place (`record.when.setTime(...)`, `record.bytes[0] = 9`) used to save; it no longer does, and no error is raised. Assign a new value instead, such as `record.when = new Date(...)`. `Date` and `Bytes` elements of an array property still save when changed in place. See [What a tracked record writes](/reference/v5/resources/resource-api#what-a-tracked-record-writes).
+
+### Tracked Records Write Only What You Change
+
+A record returned by `update()` no longer writes properties you only read: reading a sub-object or an array element saves nothing, sends nothing to a source's `patch` and adds nothing to the audit history. On a table that is not `@sealed`, assigning an undeclared property saves it even when nothing else changed.
+
+### `addTo` and `subtractFrom` Calls Combine
+
+Repeated `addTo` / `subtractFrom` calls on one property in a transaction now add up instead of the last one winning, and a delta after an assignment adds to the assigned value (the write is still an assignment, so it does not merge with concurrent writers). `subtractFrom` accepts a BigInt, and on an `Int`, `Long`, `Float` or `Number` property a BigInt delta is converted to a number (one beyond 2^53 in magnitude is rejected). See [`addTo`](/reference/v5/resources/resource-api#addtoproperty-string-value-number).
+
## Built-in Agent
### Turning Off or Host-Limiting `http_fetch`