INVALID_FIELD_FOR_INSERT_UPDATE
You tried to set a field that the platform doesn't let you set in this context — usually a formula field, a system-managed field (CreatedDate, LastModifiedDate, IsDeleted), or a field that's read-only on a particular create-vs-update path.
Also seen asINVALID_FIELD_FOR_INSERT_UPDATE·Cannot specify Id in an update call·Unable to create/update fields
A data-migration script that worked in development hits INVALID_FIELD_FOR_INSERT_UPDATE: Unable to create/update fields: CreatedDate, LastModifiedDate, IsDeleted. Please check the security settings of this field and verify that it is read/write for your profile or permission set. in production. The integration user has System Administrator permissions. The migration touches only standard fields. And yet the platform refuses.
What the platform is enforcing
Every field on every Salesforce object has a creatable / updateable flag in its metadata. The flag is set by the platform, not by your admin. It distinguishes fields you can write to from fields the platform reserves for itself.
The reserved categories:
- System-managed audit fields:
CreatedDate,CreatedById,LastModifiedDate,LastModifiedById,SystemModstamp. The platform stamps these automatically on every DML. - Formula fields: calculated on read; their value is the formula's output, so you can't set it directly.
- Auto-number fields: assigned by the platform on insert; locked thereafter.
IsDeleted: set by the platform when a record moves to the recycle bin; restored by undelete.- Read-only fields on specific paths: some fields are settable on create but read-only on update (or vice versa).
Account.Owneris an example: settable on insert via OwnerId, but on update via the OwnerId field with platform-level access checks.
If you include any of these in the field list passed to an insert or update call, the platform refuses with INVALID_FIELD_FOR_INSERT_UPDATE and names the specific fields that violated the rule.
The classic broken example
A migration script trying to preserve audit history during a data move:
public class AccountMigrator {
public static void copyFromLegacy(List<Legacy_Account__c> legacy) {
List<Account> accounts = new List<Account>();
for (Legacy_Account__c l : legacy) {
accounts.add(new Account(
Name = l.Account_Name__c,
Industry = l.Industry__c,
CreatedDate = l.Original_Created__c, // Refused
LastModifiedDate = l.Original_Modified__c, // Refused
IsDeleted = false // Refused
));
}
insert accounts;
}
}
The intent is clear: preserve the original audit timestamps from the legacy system. The platform refuses because CreatedDate, LastModifiedDate, and IsDeleted are off-limits to ordinary writes.
The fix: use the appropriate platform path
Different fields need different paths. The fix depends on which field the platform is complaining about.
For CreatedDate and CreatedById: enable the "Set Audit Fields upon Record Creation" permission in Setup → User Management Settings. Once enabled, profiles with the "Set Audit Fields upon Record Creation" user permission can write these fields, but only on insert and only for migration scenarios. The permission is meant for one-time data loads, not ongoing operations.
After enabling, the insert needs the special Database.DMLOptions payload:
List<Account> accounts = new List<Account>();
for (Legacy_Account__c l : legacy) {
accounts.add(new Account(
Name = l.Account_Name__c,
Industry = l.Industry__c,
CreatedDate = l.Original_Created__c,
CreatedById = UserInfo.getUserId()
));
}
Database.DMLOptions dml = new Database.DMLOptions();
// No special flag needed for audit fields; the user permission alone enables it.
Database.insert(accounts, dml);
For LastModifiedDate and LastModifiedById, the "Update Records with Inactive Owners" permission and the "Set Audit Fields" permission together enable the write path, but with stricter conditions.
For formula fields: you can't write to them. The fix is to write to the source fields and let the formula recalculate. If you need a stable value (because the formula is non-deterministic), redesign the formula or store the result in a non-formula field that you can write.
For auto-number fields: same as formula fields. You can't override the platform-assigned number. If the legacy system's number must be preserved, store it in a separate External_Id__c text field and use that for lookups instead of the auto-number.
For IsDeleted: never directly settable. To "restore" a deleted record, use undelete record; in Apex or the platform's recycle-bin UI. The ALL ROWS SOQL keyword can query soft-deleted records for inspection but doesn't let you flip the flag.
The fixed example
A data-migration script with the audit-field permission enabled and the formula-field path corrected:
public class AccountMigrator {
public static void copyFromLegacy(List<Legacy_Account__c> legacy) {
// Assumes "Set Audit Fields upon Record Creation" is enabled
// and the running user has the corresponding permission.
List<Account> accounts = new List<Account>();
for (Legacy_Account__c l : legacy) {
accounts.add(new Account(
Name = l.Account_Name__c,
Industry = l.Industry__c,
BillingCountry = l.Country__c,
AnnualRevenue = l.Revenue__c, // Source for any revenue formula
External_Id__c = l.Id, // Preserve legacy id
CreatedDate = l.Original_Created__c, // Now allowed via the user perm
CreatedById = UserInfo.getUserId() // Or a specific historical user id
));
}
// Insert with partial-success so one bad row doesn't poison the batch.
Database.SaveResult[] results = Database.insert(accounts, false);
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
System.debug(LoggingLevel.ERROR,
'Migration row ' + i + ' failed: ' + results[i].getErrors());
}
}
}
}
Three differences from the broken version: the audit fields are present but only the writable ones, the formula-field reference is replaced by writing to its source, and the partial-success variant of insert keeps the batch moving past individual failures.
How to discover which fields are writeable
The Object Manager in Setup shows every field's properties, including Creatable, Updateable, and Computed. For programmatic discovery, the Schema describe API returns the same data:
Schema.DescribeFieldResult dfr = Account.CreatedDate.getDescribe();
System.debug('Creatable: ' + dfr.isCreateable());
System.debug('Updateable: ' + dfr.isUpdateable());
System.debug('Calculated: ' + dfr.isCalculated());
If your code builds field lists dynamically, filter against dfr.isCreateable() before adding each field to an insert payload. The filter prevents INVALID_FIELD_FOR_INSERT_UPDATE at runtime by removing the offending fields up front.
Map<String, Schema.SObjectField> fields = Account.SObjectType.getDescribe().fields.getMap();
Set<String> writableFields = new Set<String>();
for (String fieldName : fields.keySet()) {
if (fields.get(fieldName).getDescribe().isCreateable()) {
writableFields.add(fieldName);
}
}
This pattern is especially useful for generic record-copy utilities that work across many SObjects without hardcoding which fields each object lets you write.
Insert vs update: when the rules differ
A field can be Creatable but not Updateable, or vice versa. The most common case is OwnerId:
- Insert: settable, sets the initial owner.
- Update: settable too, but with additional access checks. The new owner must be active, must have access to the object, and might trigger workflow rules tied to ownership change.
If your insert sets OwnerId and works, but the same update of the same field fails with a different but related error, the ownership-change path is probably the difference. The fix depends on the specific access check that fired.
When the error names a field you didn't reference
A surprise case: the error names LastModifiedDate but your code doesn't mention it. The likely cause is a managed package or a workflow rule that's adding LastModifiedDate to the field list internally, and the platform refuses on its behalf.
The diagnostic: run the failing operation in a Developer Sandbox with Setup Audit Trail enabled, then check the trail for "field update" entries within seconds of the failure. The trail usually names the automation that added the offending field.
A migration-specific gotcha
When you finish a data migration and the team starts ongoing operations on the migrated records, the "Set Audit Fields" permission you used for the migration is no longer appropriate. Revoke it from the integration user once the migration is complete. Leaving the permission in place means an ordinary bug or a malicious actor could fabricate audit history.
The permission is intended to be temporary. Treat it that way in your access-review process.
How this interacts with sharing rules and FLS
The compile-time check against INVALID_FIELD_FOR_INSERT_UPDATE happens against the platform's notion of "this field is writable in this context." Field-Level Security (FLS) is a separate check, evaluated at runtime per user. A field can be globally writable (no INVALID_FIELD_FOR_INSERT_UPDATE error) but inaccessible to a specific user (FLS hides it). The two checks are independent.
If your migration runs as a System Administrator with all permissions, FLS rarely blocks the write. If it runs as an integration user with restricted profile, FLS can hide fields that the global metadata marks as writable. You'd then get either INSUFFICIENT_ACCESS_OR_READONLY (different error) or, in some platform versions, a silent skip of the field.
Always run migration scripts as a user with explicit FLS coverage for the relevant fields. The cleanest way is to give the integration user a Permission Set that grants exactly the fields the migration touches and revoke the permission set when the migration completes.
When Database.upsert paints a clearer picture
Database.upsert evaluates the insert path for new rows and the update path for existing rows. If a field is creatable but not updateable, an upsert call that touches both new and existing rows partially fails. Some rows insert cleanly; some fail with INVALID_FIELD_FOR_INSERT_UPDATE on the update path.
The fix is to split the operation: do an insert for new rows and an update for existing rows, with each writing only the fields appropriate to its path. The slightly more verbose code is more predictable than a partial-success upsert.
A common pattern:
Map<String, Account> existingByExtId = new Map<String, Account>();
for (Account a : [SELECT Id, External_Id__c FROM Account WHERE External_Id__c IN :legacyIds]) {
existingByExtId.put(a.External_Id__c, a);
}
List<Account> toInsert = new List<Account>();
List<Account> toUpdate = new List<Account>();
for (Legacy_Account__c l : legacy) {
if (existingByExtId.containsKey(l.Id)) {
Account existing = existingByExtId.get(l.Id);
existing.Name = l.Account_Name__c;
existing.Industry = l.Industry__c;
toUpdate.add(existing);
} else {
toInsert.add(new Account(
External_Id__c = l.Id,
Name = l.Account_Name__c,
Industry = l.Industry__c,
CreatedDate = l.Original_Created__c
));
}
}
insert toInsert;
update toUpdate;
The insert payload includes CreatedDate (only valid on insert). The update payload omits it. No INVALID_FIELD_FOR_INSERT_UPDATE on either side.
Fields that change category over time
Some fields move between writable and read-only across releases. The classic example: when Salesforce introduces a new system-managed field (say, a derived score field), the field becomes part of the platform's reserved set. Any code that wrote to a field with the same name before the release breaks on the next deploy.
The defensive pattern is the same as the dynamic discovery technique earlier. Don't hardcode "I know this field is writable." Check DescribeFieldResult.isUpdateable() (or isCreateable()) before writing. The check is cheap and decouples your code from the field's metadata stability.
For business-critical integrations, also run a smoke test on each major Salesforce release that exercises every field your code touches. The release notes name new system-managed fields explicitly, so you can spot breaking changes early.
Visibility-affecting permissions on the receiving profile
The error message names the field that the platform refused. The fix on the field is one part; the fix on the profile is another. If a profile lacks field-level edit access to a field the migration tries to write, the platform returns the same family of error but for a different reason: not "the field is read-only globally" but "this user can't write it."
The distinguishing tell: if you run the same migration as a System Administrator and it succeeds, but as the integration user it fails, the cause is profile-level FLS, not the field's global metadata.
The fix is to grant the integration user a permission set that includes Edit access to the relevant fields. Lock the permission set down to the specific fields the migration uses; don't grant "Modify All" sweepingly.
A short note on standard versus custom fields
Standard fields (the ones Salesforce ships with each object) have their writability defined by Salesforce, not your admin. You can't change Account.LastModifiedDate.isUpdateable() from false to true.
Custom fields you create are fully writable by default. The exceptions are custom formula fields (always read-only) and custom auto-number fields (set on insert, read-only on update). Other custom field types (Text, Date, Number, Picklist, Lookup) are normally writable, subject to FLS and validation rules.
When you're hitting INVALID_FIELD_FOR_INSERT_UPDATE and the field in question is custom, the cause is almost always a formula or auto-number type. Open the field in Setup → Object Manager and check the field type. If it's a formula, the fix is to write to the source. If it's auto-number, the fix is to redesign around it.
Further reading from Salesforce
Related dictionary terms
Share this fix
Related Validation errors
DUPLICATE_VALUE: duplicate value found
ValidationYou tried to insert or update a record with a value on a unique field (External ID set to "Unique" or one of the unique standard fields like…
ENTITY_IS_DELETED: entity is deleted
ValidationYou tried to update or query a record that's in the recycle bin (soft-deleted). Either undelete it first, query with `ALL ROWS` to include d…
ENTITY_IS_LOCKED: entity is locked for approval
ValidationThe record is currently in an Approval Process and is locked for editing until the approval is granted, rejected, or recalled. The lock is e…
FIELD_CUSTOM_VALIDATION_EXCEPTION: <your validation rule's error message>
ValidationA validation rule on the object evaluated to TRUE for the record being saved, blocking the save. The text after the colon is the rule's own …
INACTIVE_OWNER_OR_USER: operation performed with inactive User
ValidationYou set a record's Owner (or another User reference) to a deactivated user. Salesforce blocks new ownership pointing at inactive users by de…