apex-blob-and-content-version

Use when Apex must create, persist, stream, or serve binary files — including uploading from LWC/REST, generating ContentVersion records, chunking large bodies, relating files to records via ContentDocumentLink, and handling the heap/payload limits that trip binary workflows. Triggers: 'ContentVersion VersionData Blob', 'upload file from LWC to Apex', 'large file heap limit Apex', 'ContentDocumentLink sharing'. NOT for generating PDFs (use pdf-generation-patterns); NOT for email attachment parsing (use apex-email-services).

Best use case

apex-blob-and-content-version is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Use when Apex must create, persist, stream, or serve binary files — including uploading from LWC/REST, generating ContentVersion records, chunking large bodies, relating files to records via ContentDocumentLink, and handling the heap/payload limits that trip binary workflows. Triggers: 'ContentVersion VersionData Blob', 'upload file from LWC to Apex', 'large file heap limit Apex', 'ContentDocumentLink sharing'. NOT for generating PDFs (use pdf-generation-patterns); NOT for email attachment parsing (use apex-email-services).

Teams using apex-blob-and-content-version should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/apex-blob-and-content-version/SKILL.md --create-dirs "https://raw.githubusercontent.com/PranavNagrecha/AwesomeSalesforceSkills/main/skills/apex/apex-blob-and-content-version/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/apex-blob-and-content-version/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How apex-blob-and-content-version Compares

Feature / Agentapex-blob-and-content-versionStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Use when Apex must create, persist, stream, or serve binary files — including uploading from LWC/REST, generating ContentVersion records, chunking large bodies, relating files to records via ContentDocumentLink, and handling the heap/payload limits that trip binary workflows. Triggers: 'ContentVersion VersionData Blob', 'upload file from LWC to Apex', 'large file heap limit Apex', 'ContentDocumentLink sharing'. NOT for generating PDFs (use pdf-generation-patterns); NOT for email attachment parsing (use apex-email-services).

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

Related Guides

SKILL.md Source

# Apex Blob And ContentVersion

Use this skill when Apex is the producer or consumer of binary file data — creating Files (ContentVersion), relating them to records (ContentDocumentLink), or moving Blobs between heap, DML, and callouts. The purpose is to keep the transaction under the heap ceiling, land the file under the right sharing, and avoid silent truncation when payloads are larger than they look.

---

## Before Starting

Gather this context before touching `ContentVersion`, `ContentDocumentLink`, or raw `Blob` DML:

- **Expected file size** — under 3 MB, 3–12 MB, 12 MB+? Synchronous Apex has a 6 MB heap ceiling; async (Queueable, Batch, Future) has 12 MB. ContentVersion itself supports up to 2.1 GB, but you cannot land one through a single synchronous `insert` if the Blob is already in heap.
- **Sharing model for the file** — public to all internal users (`AllUsers`), tied to a record (inferred via ContentDocumentLink), or private to the uploader? The `ShareType` and `Visibility` flags on `ContentDocumentLink` are the control plane, not ContentVersion itself.
- **Round-trip requirements** — does the file need to come back down from Apex unchanged? If so, any intermediate `Blob.toString()` conversion will corrupt non-UTF-8 bytes.
- **Transaction boundary** — DML that inserts ContentVersion counts toward your DML row limit. Chained inserts of 50 large files exhaust heap well before the DML cap.

---

## Core Concepts

### ContentVersion Is The File, ContentDocument Is The Envelope, ContentDocumentLink Is The Share

Every "File" in Salesforce is three linked records:

1. **ContentDocument** — metadata envelope (title, latest version Id, owner).
2. **ContentVersion** — the actual bytes in `VersionData`, plus version metadata (`Title`, `PathOnClient`, `ReasonForChange`).
3. **ContentDocumentLink** — the share: ties a `ContentDocumentId` to a `LinkedEntityId` (record, user, library) with a `ShareType` and `Visibility`.

When you `insert` a ContentVersion with no `ContentDocumentId`, Salesforce **creates** a new ContentDocument and assigns its Id — visible only by querying back after insert. When you set `ContentDocumentId` on a ContentVersion insert, you are adding a new version to an existing file.

### `VersionData` Is The Blob — And It Counts Toward Heap Twice

Setting `cv.VersionData = someBlob` keeps `someBlob` in heap. The DML that inserts `cv` serializes the blob into the request, but the heap entry persists until scope exits. If you loop and insert one ContentVersion per iteration with a 5 MB blob each, heap climbs monotonically — the GC behavior on `Blob` is conservative and rarely releases intermediate blobs before the transaction ends.

Practically: never hold more than one large `Blob` in scope at a time. Build the ContentVersion, insert it, set the local `Blob` reference to `null`, and let heap recover before the next iteration.

### ContentDocumentLink ShareType Controls Access Mode

`ShareType` values: `V` (Viewer), `C` (Collaborator), `I` (Inferred — visibility via the LinkedEntity record). `I` is the correct value for the "users who can see the Case can see the file" pattern; `V` and `C` are explicit grants that bypass record sharing.

`Visibility` on ContentDocumentLink: `AllUsers`, `InternalUsers`. Setting `AllUsers` on a file linked to a Community user record is what makes that file visible to the Experience Cloud community — a common gotcha for developers who link correctly but set Visibility = `InternalUsers` and see empty file lists in the community.

### Base64 Decode Is The LWC → Apex Upload Boundary

When an LWC reads a file via `FileReader` and sends the base64 string to an `@AuraEnabled` Apex method, the payload crosses the Apex Web Service limit at about 6 MB base64 (4.5 MB binary after decode). For larger files, the correct path is `lightning-file-upload`, the Content Create Form, or UI API `/sobjects/ContentVersion` — not a custom `@AuraEnabled` method.

---

## Common Patterns

### Upload From LWC, Land As Record File

**When to use:** LWC posts a base64 string to Apex; Apex persists the file and shares it with the originating record.

**How it works:**

```apex
public with sharing class RecordFileService {
    @AuraEnabled
    public static Id saveFile(Id recordId, String fileName, String base64Body) {
        Blob body = EncodingUtil.base64Decode(base64Body);
        if (body.size() > 4_500_000) {
            throw new AuraHandledException('File exceeds synchronous Apex limit; use lightning-file-upload for larger files.');
        }
        ContentVersion cv = new ContentVersion(
            Title = fileName,
            PathOnClient = fileName,
            VersionData = body,
            FirstPublishLocationId = recordId   // auto-creates ContentDocumentLink
        );
        insert cv;
        return [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id].ContentDocumentId;
    }
}
```

**Why not the alternative:** Inserting a separate `ContentDocumentLink` works but requires two DMLs and an extra query. `FirstPublishLocationId` asks the platform to create the link with inferred sharing automatically. If you need `V` or `C`, omit `FirstPublishLocationId` and insert the link explicitly.

### Attach A File Generated In Apex (Report / Export / Rendered PDF)

**When to use:** A Queueable or Batch generates a report or CSV and must attach it to a record.

**How it works:**

```apex
public with sharing class ReportExportQueueable implements Queueable {
    private final Id accountId;
    public ReportExportQueueable(Id accountId) { this.accountId = accountId; }

    public void execute(QueueableContext ctx) {
        Blob csvBody = Blob.valueOf(AccountCsvBuilder.build(accountId));
        ContentVersion cv = new ContentVersion(
            Title = 'Account_Export_' + accountId,
            PathOnClient = 'export.csv',
            VersionData = csvBody
        );
        insert cv;

        Id docId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id].ContentDocumentId;
        insert new ContentDocumentLink(
            ContentDocumentId = docId,
            LinkedEntityId = accountId,
            ShareType = 'V',
            Visibility = 'AllUsers'
        );
    }
}
```

**Why not the alternative:** Attaching via the legacy `Attachment` object works on paper but lacks version history, preview, and mobile support. ContentVersion is the modern path for every file requirement.

### Stream A Large File From An External System Into A ContentVersion

**When to use:** A nightly Batch job pulls large binaries (PDFs, images) from a partner API and stores them in Salesforce.

**How it works:**

```apex
public with sharing class AssetImportQueueable implements Queueable, Database.AllowsCallouts {
    private final String assetUrl;
    private final Id recordId;

    public AssetImportQueueable(String assetUrl, Id recordId) {
        this.assetUrl = assetUrl;
        this.recordId = recordId;
    }

    public void execute(QueueableContext ctx) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(assetUrl);
        req.setMethod('GET');
        req.setTimeout(120_000);
        HttpResponse res = new Http().send(req);

        if (res.getStatusCode() != 200) {
            throw new CalloutException('Asset fetch failed: ' + res.getStatus());
        }
        Blob body = res.getBodyAsBlob();
        if (body.size() == 0) {
            throw new CalloutException('Empty asset body');
        }

        ContentVersion cv = new ContentVersion(
            Title = assetUrl.substringAfterLast('/'),
            PathOnClient = assetUrl.substringAfterLast('/'),
            VersionData = body,
            FirstPublishLocationId = recordId
        );
        insert cv;
    }
}
```

**Why not the alternative:** Attempting this synchronously from an `@AuraEnabled` method will fail either at the 120-second callout timeout, the 6 MB heap ceiling, or the Apex CPU ceiling for 10+ MB downloads. Queueable with `Database.AllowsCallouts` buys you the async heap limit (12 MB) and the 120-second per-callout timeout.

---

## Decision Guidance

| Situation | Recommended Approach | Reason |
|---|---|---|
| LWC upload under 4 MB binary | `@AuraEnabled` method receiving base64 | Fits in sync heap; simplest code |
| LWC upload over 4 MB | `lightning-file-upload` or UI API direct | Bypasses Apex payload limit |
| Apex-generated file attached to a record | ContentVersion with `FirstPublishLocationId` | One insert, auto-links |
| Multiple records need the same file | Insert once, then one ContentDocumentLink per record | Avoid duplicating bytes |
| File must be visible in a community | `ContentDocumentLink.Visibility = 'AllUsers'` | `InternalUsers` hides it from Experience Cloud |
| File must follow the record's share | `ContentDocumentLink.ShareType = 'I'` | Inferred sharing respects record access |
| File generated on a schedule | Queueable with `Database.AllowsCallouts` | 12 MB async heap + callout support |
| Legacy `Attachment` object | Migrate to ContentVersion | No version history, deprecated for new work |

---

## Recommended Workflow

Step-by-step instructions for an AI agent activating this skill:

1. Confirm the **size envelope** — under 4 MB is synchronous-safe; otherwise force async or direct-to-UI-API upload.
2. Decide the **sharing model** — `FirstPublishLocationId` for inferred sharing, or explicit `ContentDocumentLink` with `ShareType` / `Visibility`.
3. Keep **only one Blob** in scope at a time — insert, null the reference, loop.
4. After `insert cv`, **query back** for `ContentDocumentId` if you need to add additional links.
5. Test with a **realistic payload size**; test harnesses that use 10-byte fixtures miss every heap bug.

---

## Review Checklist

- [ ] Every ContentVersion insert sets `Title`, `PathOnClient`, and `VersionData`.
- [ ] Sharing is explicit: `FirstPublishLocationId` OR `ContentDocumentLink` with `ShareType` + `Visibility`.
- [ ] No loop holds more than one large `Blob` in scope at a time.
- [ ] LWC uploads over 4 MB binary are routed to `lightning-file-upload` or UI API, not a custom `@AuraEnabled`.
- [ ] Callout-sourced imports run in a Queueable with `Database.AllowsCallouts`, not sync Apex.
- [ ] Community-facing files use `Visibility = 'AllUsers'`.
- [ ] Base64 encoding is not round-tripped through `Blob.toString()`.
- [ ] Heap usage is tested with realistic file sizes, not trivial fixtures.

---

## Salesforce-Specific Gotchas

1. **`FirstPublishLocationId` is ignored if `ContentDocumentId` is also set** — setting both tells Salesforce to add a new version, not create a new file. The `FirstPublishLocationId` silently drops.
2. **`ContentVersion.VersionData` is blanked on query unless you explicitly select it** — `SELECT Id, Title FROM ContentVersion` returns the row without the bytes. Code that assumes the bytes are there will NullPointerException.
3. **`ContentDocumentLink.Visibility = 'InternalUsers'` hides the file from Experience Cloud users even when they own the linked record** — a common "why can't my community user see the file" misfire.
4. **Base64 encoding inflates payloads by ~33%** — a 6 MB binary becomes ~8 MB base64 over the wire. The LWC → Apex Web Service payload limit is measured on the base64 form.
5. **`insert` on ContentVersion with `FirstPublishLocationId = userId`** files the document to that user's private library, not a shared record — useful when deliberate, confusing when not.
6. **The legacy `Attachment` object still works but cannot coexist cleanly with modern file sharing** — Files promoted to ContentVersion cannot go back to Attachment; the migration is one-way.
7. **`EncodingUtil.base64Decode` on a string that includes header prefix `data:image/png;base64,...` silently returns corrupted bytes** — strip the prefix before decoding.
8. **Sharing a file via ContentDocumentLink requires visibility of the ContentDocument** — a trigger handler running as an unprivileged user can fail to add links it could logically see.

---

## Output Artifacts

| Artifact | Description |
|---|---|
| File upload / save Apex | `@AuraEnabled` or REST method creating ContentVersion + link with correct sharing |
| Async file import class | Queueable / Batch for large files, callouts, or scheduled imports |
| Review findings | List of heap risks, sharing misconfigurations, and legacy `Attachment` uses |

---

## Related Skills

- `apex/pdf-generation-patterns` — use when generating PDFs via Visualforce `renderAs="pdf"` before landing them as files.
- `apex/apex-email-services` — use when the binary source is an inbound email attachment.
- `apex/apex-cpu-and-heap-optimization` — use alongside when the path carries multiple large blobs per transaction.
- `apex/callouts-and-http-integrations` — use when the binary comes from an outbound HTTP GET.
- `apex/apex-encoding-and-crypto` — use alongside when the Blob must be encrypted, signed, or hashed before storage.

Related Skills

apex-managed-sharing-patterns

8
from PranavNagrecha/AwesomeSalesforceSkills

Grant row-level access programmatically via __Share records when declarative sharing rules cannot express the policy. NOT for OWD, role hierarchy, or criteria-based sharing rule design.

omniscript-versioning

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when managing OmniScript versions: activating new versions, deactivating prior versions, testing a specific version before activation, rolling back to a previous version, or understanding version identity (Type/Subtype/Language triplet). NOT for OmniStudio deployment or DataPack migration (use omnistudio/omnistudio-deployment-datapacks).

lwc-imperative-apex

8
from PranavNagrecha/AwesomeSalesforceSkills

Call Apex methods imperatively from LWC — on button click, lifecycle hooks, or conditional logic. Covers import syntax, cacheable vs non-cacheable, async/await patterns, error handling, loading states, and Promise.all. NOT for wire service (use wire-service-patterns) and NOT for testing Apex mocks (use lwc-testing).

dataweave-for-apex

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when transforming structured data inside Apex — CSV → JSON, XML → SObject list, JSON → flattened CSV, or schema-mapping a third-party payload to a Salesforce model — and the existing options (`JSON.deserialize`, `Dom.Document`, hand-written loops) are getting unwieldy. Triggers: 'apex transform csv json xml without external library', 'system.dataweave script', 'salesforce native dataweave apex execute', 'transform xml to sobject apex no mulesoft', 'json reshape salesforce apex script'. NOT for MuleSoft Anypoint DataWeave running off-platform (use mulesoft-anypoint-architecture), NOT for Apex JSON serialization basics (use apex-json-serialization), NOT for Bulk API CSV ingest (use bulk-api-2-patterns).

api-versioning-strategy

8
from PranavNagrecha/AwesomeSalesforceSkills

Design versioning for custom Apex REST endpoints: URI versioning, backward compatibility, deprecation sunset. NOT for consuming external APIs.

flow-versioning-strategy

8
from PranavNagrecha/AwesomeSalesforceSkills

Manage Flow versions: activation policy, paused interview compatibility, cleanup cadence, and breaking-change detection. Trigger keywords: flow version management, activate flow version, paused interview, flow cleanup, flow breaking change, flow rollback. Does NOT cover: FlowDefinition metadata deploy order (see devops skill), Process Builder retirement, or Flow test coverage (separate skill).

flow-record-locking-and-contention

8
from PranavNagrecha/AwesomeSalesforceSkills

Diagnose and prevent UNABLE_TO_LOCK_ROW + parent-record contention in record-triggered, scheduled, and screen flows by mapping the implicit lock chain and applying decouple patterns (Platform Events, Queueable handoff, Scheduled Paths). NOT for general flow bulkification — see flow-bulkification. NOT for fault-path catch logic — see flow-rollback-patterns.

flow-invocable-from-apex

8
from PranavNagrecha/AwesomeSalesforceSkills

Author @InvocableMethod Apex classes that Flow can call as Actions. Design the input / output variable contract, bulk semantics (one list in, one list out), null handling, and error surfacing. Also covers the inverse direction: calling a flow from Apex via Flow.Interview. NOT for general Apex authoring (use apex-service-selector-domain). NOT for REST-exposed Apex (use apex-rest-resource-patterns).

flow-apex-defined-types

8
from PranavNagrecha/AwesomeSalesforceSkills

Design and use Apex-Defined Types as Flow variables for structured non-sObject data (HTTP callout payloads, External Service responses, complex configuration). Trigger keywords: apex-defined type, flow variable, @AuraEnabled class, flow http callout response. Does NOT cover building HTTP Callout Actions themselves, External Services schema, or raw Apex invocable methods.

api-version-management

8
from PranavNagrecha/AwesomeSalesforceSkills

Use this skill when auditing, upgrading, or standardizing Salesforce API versions across metadata components, sfdx-project.json sourceApiVersion, Apex classes, LWC bundles, Aura bundles, and integration endpoints. Covers version drift detection, retirement risk analysis, and upgrade planning. NOT for REST/SOAP API design patterns (use rest-api-patterns or soap-api-patterns), OAuth configuration (use oauth-flows-and-connected-apps), or Metadata API deployment mechanics (use change-set-deployment or unlocked-package-development).

scheduled-apex-failure-detection-and-monitoring

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when nightly batch / scheduled Apex jobs are failing without anyone noticing — covers why uncaught exceptions in `execute()` go to the debug log instead of email, how to query `AsyncApexJob` for `Status`, `NumberOfErrors`, and `ExtendedStatus`, when to implement `Database.RaisesPlatformEvents` so the platform publishes `BatchApexErrorEvent` on uncaught failures, how to subscribe to that event with an Apex trigger and notify operators, and how to layer a custom watcher schedule on top so silent-failure modes (job that never started, scheduled class deleted, queue stuck on `Queued`) still surface. Triggers: 'nightly batch failed at 2am with no notification', 'how do we know if a scheduled apex job is failing', 'BatchApexErrorEvent vs custom retry logic', 'Setup Apex Jobs only shows last 7 days, where else can I look', 'job is stuck in queued status nobody noticed for a week'. NOT for general Apex exception handling patterns (use apex/apex-exception-handling-and-logging), NOT for Batch Apex authoring or chunking strategy (use apex/batch-apex-design), NOT for Setup → Apex Jobs UI walkthrough as an admin task (use admin/batch-job-scheduling-and-monitoring), NOT for retry logic itself (use apex/scheduled-apex-retry-patterns once authored).

record-locking-and-contention

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when diagnosing or preventing UNABLE_TO_LOCK_ROW errors, record lock contention in Apex or Bulk API loads, FOR UPDATE locking, parent-child lock escalation, and deadlock scenarios. Triggers: 'UNABLE_TO_LOCK_ROW', 'record lock contention', 'FOR UPDATE SOQL', 'deadlock', 'lock timeout'. NOT for approval-process record locking (admin/approval-processes) or optimistic concurrency via timestamps.