custom-iterators-and-iterables
Use this skill when implementing the Iterable<T> or Iterator<T> interfaces in Apex to create custom traversal logic, build lazy-evaluation data sources for Batch Apex, or stream large result sets without materializing an entire List. Trigger keywords: custom iterator, Iterable interface, Iterator interface, batch start iterable, lazy evaluation apex, streaming apex query, paginated batch. NOT for standard list iteration (use for-each on List directly), NOT for Batch Apex fundamentals (use batch-apex-patterns), NOT for Apex triggers or synchronous bulk patterns.
Best use case
custom-iterators-and-iterables is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Use this skill when implementing the Iterable<T> or Iterator<T> interfaces in Apex to create custom traversal logic, build lazy-evaluation data sources for Batch Apex, or stream large result sets without materializing an entire List. Trigger keywords: custom iterator, Iterable interface, Iterator interface, batch start iterable, lazy evaluation apex, streaming apex query, paginated batch. NOT for standard list iteration (use for-each on List directly), NOT for Batch Apex fundamentals (use batch-apex-patterns), NOT for Apex triggers or synchronous bulk patterns.
Teams using custom-iterators-and-iterables should expect a more consistent output, faster repeated execution, less prompt rewriting, better workflow continuity with your supporting tools.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
- You already have the supporting tools or dependencies needed by this skill.
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
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/custom-iterators-and-iterables/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How custom-iterators-and-iterables Compares
| Feature / Agent | custom-iterators-and-iterables | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Use this skill when implementing the Iterable<T> or Iterator<T> interfaces in Apex to create custom traversal logic, build lazy-evaluation data sources for Batch Apex, or stream large result sets without materializing an entire List. Trigger keywords: custom iterator, Iterable interface, Iterator interface, batch start iterable, lazy evaluation apex, streaming apex query, paginated batch. NOT for standard list iteration (use for-each on List directly), NOT for Batch Apex fundamentals (use batch-apex-patterns), NOT for Apex triggers or synchronous bulk patterns.
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.
SKILL.md Source
# Custom Iterators and Iterables
Use this skill when a Batch Apex job needs a non-SOQL data source for its `start()` method, or when you want to traverse records lazily without materializing a full `List<SObject>` in heap. It covers the `Iterator<T>` and `Iterable<T>` interfaces, their contracts, patterns for lazy evaluation, and the constraints that apply when using them inside Batch Apex.
---
## Before Starting
- Confirm whether the data source can be expressed as a single SOQL query. If it can, `Database.getQueryLocator()` is almost always the right choice because it streams results transparently and is exempt from the 50,000-row SOQL query limit. Only reach for a custom Iterable when the data source cannot be a single SOQL query.
- Understand that when `start()` returns an `Iterable`, the total records the platform can handle across all batches is bounded by the 50,000-row query-rows-returned governor limit, not the separate query locator cursor limit. This is a critical difference.
- Custom Iterators cannot be used directly in Apex for-each loops unless they are wrapped inside a class that also implements `Iterable<T>`. An `Iterator<T>` alone is not iterable by the for-each construct — the for-each loop requires `Iterable<T>`.
- Heap usage is your primary risk: if `iterator()` pre-materializes the entire collection at construction time, you have not gained anything over a plain `List`. Genuine lazy evaluation means `next()` fetches or computes one element (or a small page) at a time.
---
## Core Concepts
### The Iterator<T> Interface
`Iterator<T>` has exactly two methods: `Boolean hasNext()` and `T next()`. Both must be declared `global` or `public`. The platform calls `hasNext()` before calling `next()`; the contract is that `next()` must not be called when `hasNext()` returns `false`. Violating this contract throws a `NoSuchElementException` at runtime. The type parameter `T` is most commonly `SObject` or a concrete sObject type when used with Batch Apex, but can be any Apex type.
### The Iterable<T> Interface
`Iterable<T>` has a single method: `Iterator<T> iterator()`. When a class implements `Iterable<T>`, it can appear in an Apex for-each loop and can be returned from `Database.Batchable<T>.start()`. The `iterator()` method is called once per loop or once by the batch executor to obtain the iterator cursor. It should create and return a fresh `Iterator<T>` each time so the collection can be traversed multiple times independently.
### Batch Apex start() with Iterable
When `start(Database.BatchableContext bc)` returns `Iterable<SObject>` instead of `Database.QueryLocator`, the batch framework calls `iterator()` on the return value to enumerate all records, then chunks them into execute() batches of the configured scope size. The key behavioral difference from `QueryLocator` is that the full enumeration is subject to standard Apex governor limits, including the 50,000-row query-rows-returned cap per transaction in the `start()` execution context. The batch framework does not bypass this limit for custom iterables the way it bypasses it for `QueryLocator`.
### Lazy Evaluation Pattern
A lazy iterator defers fetching or computing the next element until `next()` is called. A common pattern is a SOQL-offset paginator: the iterator holds a current offset and page buffer. When the buffer is exhausted, `next()` triggers the next SOQL query to refill the buffer. This spreads heap usage over time but introduces additional SOQL queries. Because `start()` runs in a single transaction with its own governor limit context, the total number of rows returned across all paginated queries still accumulates against the 50,000-row query-rows-returned limit.
---
## Common Patterns
### Pattern 1 — Custom Iterable Wrapping a Pre-Computed List
**When to use:** The records to process are produced by Apex logic (e.g., aggregated results, cross-object computations, external API responses) that cannot be expressed as a SOQL query locator, but the total record count is known to stay well within the 50,000-row query-rows-returned limit.
**How it works:** Produce the `List<SObject>` in the `iterator()` method (not at construction time) and return a list-backed iterator that walks the list with an integer index.
```apex
public class ComputedAccountIterable implements Iterable<Account> {
private final List<Id> accountIds;
public ComputedAccountIterable(List<Id> ids) {
this.accountIds = ids;
}
public Iterator<Account> iterator() {
// Fetch at iteration time, not at construction time
return [SELECT Id, Name FROM Account WHERE Id IN :accountIds].iterator();
}
}
```
Note: `List<T>.iterator()` returns a built-in `Iterator<T>`, so wrapping a built-in list is zero boilerplate.
**Why not the alternative:** Passing `accountIds` as a bind variable to `Database.getQueryLocator()` is actually simpler when the IDs come from a prior Apex step. Use the custom Iterable pattern only when the data source is not a query at all — for example, records synthesized from a REST callout or an aggregation.
### Pattern 2 — Stateful Custom Iterator with Internal Cursor
**When to use:** The data source is externally paginated (e.g., records fetched from an external API), or when you need per-element transformation logic before the batch framework sees each record.
**How it works:** Implement `Iterator<SObject>` directly with an internal page buffer and a done flag.
```apex
public class PagedExternalIterator implements Iterator<Account> {
private List<Account> buffer = new List<Account>();
private Integer bufferIndex = 0;
private Integer offset = 0;
private static final Integer PAGE_SIZE = 200;
private Boolean exhausted = false;
public Boolean hasNext() {
if (bufferIndex < buffer.size()) {
return true;
}
if (exhausted) {
return false;
}
// Refill buffer
buffer = [SELECT Id, Name FROM Account ORDER BY CreatedDate LIMIT :PAGE_SIZE OFFSET :offset];
bufferIndex = 0;
offset += buffer.size();
if (buffer.isEmpty()) {
exhausted = true;
return false;
}
return true;
}
public Account next() {
if (!hasNext()) {
throw new NoSuchElementException('No more elements');
}
return buffer[bufferIndex++];
}
}
public class PagedExternalIterable implements Iterable<Account> {
public Iterator<Account> iterator() {
return new PagedExternalIterator();
}
}
```
**Why not the alternative:** A single SOQL query with an OFFSET clause becomes inefficient past roughly 2,000 rows because the database must skip scanned rows. For very large sets, prefer `QueryLocator`. For moderate sets where a single SOQL query is architecturally impossible, pagination inside a custom iterator spreads the work.
---
## Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Data comes from a single SOQL query on sObjects | `Database.getQueryLocator()` | Exempted from the 50 k query-rows limit; cursor managed by platform |
| Records produced by Apex logic or external callout | Custom `Iterable<SObject>` in `start()` | Only option when a SOQL query locator cannot represent the source |
| Need for-each loop over custom Apex objects | Implement `Iterable<T>` on the class | For-each requires `Iterable<T>`, not `Iterator<T>` alone |
| Total record count < 50,000 and source is pre-computed | `List<SObject>.iterator()` wrapped in an `Iterable` | Simplest path; built-in list iterator handles cursor |
| Large paginated external source where count is unknown | Stateful custom `Iterator` with lazy SOQL pagination | Avoids materializing full set; fetches per page on demand |
| Multi-object or aggregated source requiring joins | Custom `Iterator` that merges multiple lists | QueryLocator only supports single-query sources |
---
## Recommended Workflow
Step-by-step instructions for an AI agent or practitioner activating this skill:
1. Gather context — confirm the org edition, relevant objects, and current configuration state
2. Review official sources — check the references in this skill's well-architected.md before making changes
3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
4. Validate — run the skill's checker script and verify against the Review Checklist below
5. Document — record any deviations from standard patterns and update the template if needed
---
## Review Checklist
- [ ] `hasNext()` never throws; it returns `false` when exhausted
- [ ] `next()` throws `NoSuchElementException` (not returns `null`) when called past end
- [ ] `iterator()` on the `Iterable` creates a fresh cursor each call — no shared mutable state between calls
- [ ] Total rows returned through the iterator in the `start()` context stay within the 50,000-row query-rows-returned governor limit
- [ ] Heap usage has been estimated: the buffer size and object size stay within the 12 MB async heap limit
- [ ] The Batch class implements `Database.Batchable<SObject>` and `start()` return type matches the iterator element type
- [ ] If `Database.Stateful` is needed for accumulating results, it is declared on the Batch class, not the Iterator class
---
## Salesforce-Specific Gotchas
1. **Custom Iterators are subject to the 50,000-row query limit in start()** — Unlike `Database.getQueryLocator()`, which runs in a separate cursor context exempt from the query-rows-returned limit, a custom `Iterable` returned from `start()` runs inside the batch's `start()` transaction. Every SOQL row that `next()` fetches counts against the 50,000-row-per-transaction limit in that context. If your iterable pages through 60,000 records via SOQL, the job fails with `System.LimitException: Too many query rows: 50001`.
2. **Iterator<T> alone cannot be used in a for-each loop** — Apex for-each requires the target to implement `Iterable<T>`. An `Iterator<T>` by itself is not iterable. A common mistake is implementing only `Iterator<T>` and then using `for (SObject o : myIterator)`, which produces a compile error. Always pair `Iterator<T>` with a wrapping `Iterable<T>` when for-each syntax is required.
3. **Mutable state in iterator() causes ghost-replay bugs** — If the `Iterable` class stores the iterator's position as an instance variable and `iterator()` returns `this` rather than a new cursor, the second caller (or the second batch job enqueue) resumes from where the first left off instead of starting from the beginning. Always return `new MyIterator(...)` from `iterator()` rather than re-using a stored cursor.
4. **OFFSET-based pagination degrades at scale** — SOQL `OFFSET` scans and discards leading rows on every query, making it progressively slower and more CPU-intensive as the offset grows. Beyond roughly 2,000 records, use keyset pagination (`WHERE Id > :lastId ORDER BY Id`) instead of `OFFSET` inside a custom iterator.
5. **Scope size still applies; hasNext() may be called more times than expected** — The batch framework calls `hasNext()` and `next()` to fill each batch scope slice. With a scope of 200, `next()` is called 200 times before `execute()` runs. If `hasNext()` triggers a SOQL refill on every call (instead of buffering), you will exhaust SOQL-per-transaction limits quickly. Always buffer a full page before returning `true` from `hasNext()`.
---
## Output Artifacts
| Artifact | Description |
|---|---|
| Iterator<SObject> implementation | Apex class with `hasNext()` and `next()` carrying a lazy page buffer |
| Iterable<SObject> wrapper | Apex class returning a fresh cursor from `iterator()` for use in Batch start() or for-each loops |
| Batch Apex class using custom Iterable | Batch class whose `start()` returns the custom Iterable with correct type parameter |
| Decision table | Guidance on QueryLocator vs. custom Iterable vs. pre-computed List for a given scenario |
---
## Related Skills
- `batch-apex-patterns` — Covers Batch Apex structure, scope tuning, `Database.Stateful`, and `QueryLocator` patterns; use alongside this skill when designing the full batch job
- `apex-queueable-patterns` — When record volume or complexity is moderate, Queueable chaining may avoid the need for Batch + Iterable altogetherRelated Skills
batch-apex-patterns
Use when designing, reviewing, or debugging Batch Apex contracts, scope sizing, stateful behavior, chaining, and AsyncApexJob monitoring. Triggers: 'Database.Batchable', 'Database.Stateful', 'executeBatch', 'batch scope', 'AsyncApexJob'. NOT for generic async choice discussions where Queueable or Future might still be the better tool.
customer-data-request-workflow
Implement GDPR/CCPA data subject rights (access, deletion, rectification) using Salesforce Privacy Center and/or custom workflow. NOT for general backup or org-level data retention policy.
omnistudio-custom-lwc-elements
Creating and integrating custom Lightning Web Components within OmniScripts: LWC override patterns, pubsub event handling, custom validation, OmniStudio data passing conventions. Use when a standard OmniScript element cannot meet a UX requirement. NOT for standalone LWC development (use lwc/* skills). NOT for Integration Procedures (use integration-procedures). NOT for embedding an OmniScript inside an LWC (use omnistudio-lwc-integration).
lwc-custom-lookup
Custom lookup component in LWC — typeahead/autocomplete that searches records via Apex SOSL/SOQL, shows pills, supports keyboard navigation, and manages open/close state. Use when lightning-input-field or lightning-record-picker won't work (cross-org search, computed filters, custom result rendering). NOT for in-form lookups inside lightning-record-edit-form (use lightning-input-field) or lookup filters (use admin lookup filter config).
lwc-custom-event-patterns
When and how to design CustomEvent traffic out of an LWC — bubbles / composed / cancelable flag choices, detail payload shape, naming rules, and propagation control. Trigger keywords: 'event not reaching parent', 'composed shadow DOM', 'CustomEvent detail mutation', 'stopPropagation vs stopImmediatePropagation'. NOT for parent-to-child communication (use `@api` — see `lwc/component-communication`), NOT for sibling fan-out (use Lightning Message Service — see `lwc/lightning-message-service`), NOT for wire-service data plumbing.
lwc-custom-datatable-types
Use when you need to extend `lightning-datatable` with custom cell renderings: status pills, progress bars, image thumbnails, action cells, editable pickliststo, rich-text, or any column that `lightning-datatable` does not ship out of the box. Triggers: 'custom cell type lightning datatable', 'progress bar column', 'image column', 'inline edit picklist in datatable', 'rich text column'. NOT for basic datatable usage (see `lwc-data-table`) and NOT for tree-grid or large-dataset virtualization (see `virtualized-lists`).
experience-cloud-search-customization
Use this skill when configuring or extending search on an Experience Cloud site — covering Search Manager scope configuration, LWR vs Aura search component selection, federated search setup, guest user search access, and custom search result components. NOT for SOSL/SOQL query development. NOT for internal Salesforce global search or Einstein Search for agents.
custom-property-editor-for-flow
Use when building or reviewing an LWC Custom Property Editor for Flow screen or action configuration, including the `configurationEditor` metadata hook, builder-side APIs, validation, and value-change events. Triggers: 'custom property editor', 'Flow configuration editor', 'builderContext', 'inputVariables', 'configurationEditor'. NOT for ordinary runtime screen-component behavior when no Flow Builder design-time customization is involved.
flow-custom-property-editors
Use when designing or reviewing Flow custom property editor patterns for screen components or actions, including when Flow Builder needs guided design-time configuration, generic type mapping, or builder-context-aware validation. Triggers: 'Flow custom property editor', 'configurationEditor', 'builderContext', 'inputVariables', 'Flow screen component setup'. NOT for general LWC runtime behavior when Flow Builder customization is not involved.
feature-flag-custom-metadata
Implement environment-safe feature flags using Custom Metadata Types for Apex, LWC, and Flow. NOT for user-level entitlements or permission sets.
custom-index-requests
Use when standard Salesforce indexes are insufficient for query performance on high-volume objects: requesting custom indexes via Salesforce Support for standard fields, deploying custom field indexes via Metadata API, requesting skinny tables, or designing two-column composite indexes. NOT for SOQL query optimization techniques (use data/soql-query-optimization) or general large data volume design (use architect skills).
customer-effort-scoring
Designing a customer experience measurement strategy using CSAT, NPS, and CES metrics in Salesforce Feedback Management: metric selection, survey timing, Feedback Management license requirements, Flow-triggered survey delivery, and CX KPI integration into Service Analytics dashboards. Use when choosing which CX metric to deploy, defining survey timing rules, or connecting survey response data to operational KPIs. NOT for building or configuring the technical survey objects, flows, or email templates (use admin/salesforce-surveys for that). NOT for custom programmatic survey delivery via API.