From ensembles
Assists with building, debugging, and migrating apps using the Ensembles 3 Swift sync framework for Core Data and SwiftData event-sourcing synchronization.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ensembles:ensemblesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Event-sourcing Core Data / SwiftData sync framework; successor to Ensembles 2 (ObjC). Local saves are captured as events in a separate SQLite event store, exchanged through a pluggable `CloudFileSystem`, and replayed into the user's store. This skill pins the exact API and the handful of things that are easy to get wrong.
Event-sourcing Core Data / SwiftData sync framework; successor to Ensembles 2 (ObjC). Local saves are captured as events in a separate SQLite event store, exchanged through a pluggable CloudFileSystem, and replayed into the user's store. This skill pins the exact API and the handful of things that are easy to get wrong.
The designated init is failable and takes a model object, not a URL (URL-based convenience inits also exist):
import EnsemblesLocalFile // @_exported brings in Ensembles too
let cloud = LocalCloudFileSystem(rootDirectory: sharedFolderURL)
let ensemble = CoreDataEnsemble(
ensembleIdentifier: "MyStore",
persistentStoreURL: storeURL,
managedObjectModel: model, // NSManagedObjectModel
managedObjectModels: nil, // pass all model versions for versioning, else nil
cloudFileSystem: cloud
)! // returns nil if a store URL is already in use
try await ensemble.attachPersistentStore() // seedPolicy defaults to .mergeAllData
try await ensemble.sync()
SwiftData: use SwiftDataEnsemble (factory, builds the model from @Model types; iOS 17+/macOS 14+).
Add the Swift package:
.package(url: "https://github.com/mentalfaculty/Ensembles3.git", from: "3.0.4")
Then depend on the products you need (Ensembles, EnsemblesCloudKit, …). This is the binary distribution; CloudKit and local sync are free, other backends need a licence. Premium customers with a source licence use the Ensembles3-Source package instead, which also enables package traits for the SDK-backed backends (Dropbox, S3, Box, Zip, Multipeer).
| Call | Purpose |
|---|---|
attachPersistentStore(seedPolicy:) | Register store, join the ensemble. Was E2 leech. |
sync(options:) | One sync pass: export local, import remote, integrate. Was E2 merge. |
detachPersistentStore() | Leave the ensemble. Was E2 deleech. |
Operations are serialized internally (AsyncStream); concurrent calls queue safely. CoreDataEnsembleDelegate is for hooks (conflict/merge lifecycle, error handling, custom global identifiers), not for driving operations. For reliable cross-device propagation, two rounds are common: sync() to export, then sync() again after peers have exported, to import.
Two cases only — mergeAllData (default) and excludeLocalData. Always recommend mergeAllData (merges existing local data into the ensemble); don't present it as a decision. Use excludeLocalData only when intentionally discarding local data (delete the store first).
attachPersistentStore deletes the event-data dir and rejoins as a fresh peer (new persistentStoreIdentifier); cloud is source of truth. Unsynced local E2 events are lost — sync E2 to completion before upgrading.EnsembleError.cloudIdentityChanged — expected, recover by re-attaching.compatibilityMode: .ensembles2Compatible while an E2 fleet still exists; it only restricts writes (reads handle both formats). CloudKit gotcha: E2/E3 inits can target different zones — mixed fleets must match.If a transformable attribute (e.g. a CLLocation stored via a value transformer) arrives nil on the receiving device while the rest of the object syncs fine, check, in order:
@objc(YourTransformerName) NSSecureUnarchiveFromDataTransformer subclass with the stored class in allowedTopLevelClasses, and call its registration early (e.g. in App.init / application(_:didFinishLaunching…)), before you build the container. With setLoggingLevel(.verbose), a Failed to retrieve value transformer: line confirms this is the cause.awakeFromInsert — see below. If it assigns the attribute from a current-device source, it can overwrite the synced value during integration.When Ensembles applies changes synced from another device, it inserts objects into a context of its own, and Core Data calls awakeFromInsert() on them just as it does for objects your app creates. If your awakeFromInsert() assigns content from the current device (the current location, a timestamp, a status), that assignment can overwrite the value arriving from the other device. Guard it.
Guard current-device content with the public NSManagedObjectContext.isEnsemblesIntegrationContext accessor:
override func awakeFromInsert() {
super.awakeFromInsert()
guard managedObjectContext?.isEnsemblesIntegrationContext != true else { return }
if uniqueIdentifier == nil { uniqueIdentifier = UUID().uuidString }
if let loc = LocationProvider.shared.current { self.location = loc }
}
Assigning a stable global identifier need not be guarded; guarding content attributes is what matters. When reviewing or writing a Syncable/Core Data model class, always check awakeFromInsert (and awakeFromFetch) for unguarded current-device assignments.
Out of the box (no extra deps): CloudKit, LocalFile, Memory, iCloudDrive (deprecated), GoogleDrive, OneDrive, pCloud, WebDAV, Encrypted, Supabase. Trait-gated (only fetched when the trait is enabled in Package.swift): Dropbox, S3, Box, Zip, Multipeer.
Free backends: CloudKitFileSystem, LocalCloudFileSystem, MemoryCloudFileSystem. All others require a license: call EnsemblesLicense.activate("<key>") before attaching, or attach throws EnsembleError.unlicensed.
Use MemoryCloudFileSystem as the backend in tests — it's an in-memory cloud, so two ensembles sharing one instance sync without touching CloudKit or the filesystem. Drive a save on one, sync() both, and assert the data arrived on the other.
CKDatabase (_record(for:), _save, _recordZone(for:)), or "unexpected ':' in type" errors from a .swiftinterface file, when building against the binary package: you are on 3.0.2 or 3.0.3, whose binaries were built with a beta toolchain and cannot be consumed from stable Xcode. Update to 3.0.4 or later. The source package never had this problem.Library/Caches/CloudKit grows far larger than the store: fixed in 3.0.2, and 3.0.3 makes the first sync after a relaunch fast as well (it restores a persisted listing of the zone and fetches only changes). Update to 3.0.3+..cdecloudkitcache.v3 file in the app's Caches directory is Ensembles' persisted listing of the CloudKit zone. It is safe to delete — the only effect is one full (metadata-only) refetch on the next sync. Do not delete the event store itself.NSManagedObjectModel (URL convenience inits exist, but the designated init takes a model).init?) — it returns nil if the store URL is already registered to another ensemble.unlicensed — only CloudKit/Local/Memory are free.sync() propagates both ways instantly — often needs an export round then an import round.awakeFromInsert that re-stamps current-device content (location/timestamp/status) during integration without guarding isEnsemblesIntegrationContext — can overwrite synced values.npx claudepluginhub mentalfaculty/ensembles3 --plugin ensemblesImplements and debugs CloudKit sync for iOS/macOS apps: container setup, CKRecord CRUD, queries, subscriptions, CKSyncEngine, SwiftData integration, conflict resolution, and iCloud account status checks.
Provides expert Core Data guidance for iOS/macOS: stack setup, fetch requests & NSFetchedResultsController, saving/merge conflicts, threading/Swift Concurrency, batch ops/persistent history, migrations, performance, CloudKit sync.
Guides data persistence on Apple platforms: SwiftData, Core Data, GRDB, SQLite, CloudKit, file storage, Codable, migrations. Includes auditors for schema safety and migration debugging.