# Updates and deletes

Partial updates that send only what changed, diff-driven writes and deleting records by ID.

## Updating a record {#updating-a-record}

An update is a PATCH on the record's path, and it is always partial - only the fields to change go in the request and everything else stays as it was. There is no read-modify-write cycle and no risk of overwriting fields you never meant to touch:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class CloseOpportunity(Service):
    name = 'crm.close-opportunity'

    input = 'opportunity_id'

    def handle(self):

        opportunity_id = self.request.input.opportunity_id

        conn = self.salesforce['My Salesforce Connection']

        # Only the fields that change go into the request ..
        changes = {
            'StageName': 'Closed Won',
        }

        # .. and everything else stays untouched.
        _ = conn.patch(f'/sobjects/Opportunity/{opportunity_id}', changes)
```

A successful update answers with HTTP 204 and no content - conn.patch returns an empty dict. Anything else is an error, delivered in the [error array format](https://zato.io/docs/dev/salesforce/error-handling-and-api-limits.html).

## Writing only the changed fields {#writing-only-the-changed-fields}

In a sync that runs on a schedule, most records have not changed since the previous run. Updating them all anyway burns [API requests](https://zato.io/docs/dev/salesforce/error-handling-and-api-limits.html), fires the org's automation - triggers, flows, field history - for records where nothing happened, and makes every run look like a mass modification in the audit trail.

The pattern that avoids this is compare-then-write: build the desired values, compare them field by field with what Salesforce currently holds, and send a PATCH only for the fields that differ:

```python
# The values this record should have, keyed by Salesforce field name
desired = {
    'StageName': stage_name,
    'Amount': amount,
    'Partner_Code__c': partner_code,
}

# Compare with the record as Salesforce returned it from a query ..
changes = {}

for field_name, new_value in desired.items():
    if record[field_name] != new_value:
        changes[field_name] = new_value

# .. and write only if at least one field differs.
if changes:
    opportunity_id = record['Id']
    changed_fields = sorted(changes)

    _ = conn.patch(f'/sobjects/Opportunity/{opportunity_id}', changes)
    self.logger.info('Updated %s, fields: %s', opportunity_id, changed_fields)
```

Over a whole run, logging which fields changed per record gives you an exact, reviewable account of what the sync did - and a run in which no record changed makes zero write calls. The [CRM sync scenario](https://zato.io/docs/dev/salesforce/crm-sync-scenario.html) uses this pattern throughout.

## Deleting a record {#deleting-a-record}

A delete is a DELETE on the record's path:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class DeleteCampaign(Service):
    name = 'crm.delete-campaign'

    input = 'campaign_id'

    def handle(self):

        campaign_id = self.request.input.campaign_id

        conn = self.salesforce['My Salesforce Connection']

        # Salesforce moves the record to the recycle bin and answers with 204 No Content.
        _ = conn.delete(f'/sobjects/Campaign/{campaign_id}')
```

Deleted records go to the org's recycle bin, where they stay for 15 days before permanent removal, so an accidental delete is recoverable by an administrator.

Deleting a record that does not exist - or was already deleted - fails with a NOT\_FOUND error. In integrations where deletes may be replayed, treat that error as success: the caller asked for the record to be removed, and it no longer exists.

## Prefer marking over deleting {#prefer-marking-over-deleting}

In long-lived integrations, an actual delete is rarely what the business wants - a cancelled deal or a lapsed partner usually should remain visible in reports and history. The convention that keeps everyone happy is a status field: instead of conn.delete, PATCH a field such as Status\_\_c to a value like Inactive or Do not sync, and filter on it in your [SOQL queries](https://zato.io/docs/dev/salesforce/soql-relationships-and-pagination.html). Reserve real deletes for data that was wrong in the first place, such as duplicates from an import.

## Learn more {#learn-more}

- [Salesforce integration overview](https://zato.io/integrations/salesforce/) - Start here to see how teams build Salesforce integrations
- [The REST API from Python](https://zato.io/docs/dev/salesforce/rest-api-python.html) - Connected apps, credentials, connections and the self.salesforce API
- [Create and query records](https://zato.io/docs/dev/salesforce/create-and-query-records.html) - A REST channel that creates campaigns and reads them back
- [SOQL relationships and pagination](https://zato.io/docs/dev/salesforce/soql-relationships-and-pagination.html) - Parent traversal in the select list and the nextRecordsUrl loop
- [Upserts and external IDs](https://zato.io/docs/dev/salesforce/upserts-and-external-ids.html) - Idempotent create-or-update, keyed by your own identifiers
- [Error handling and API limits](https://zato.io/docs/dev/salesforce/error-handling-and-api-limits.html) - Error arrays, failure policies and the daily request allowance
- [A CRM sync, end to end](https://zato.io/docs/dev/salesforce/crm-sync-scenario.html) - A scheduled one-way sync with correlation keys and drift detection
