From automerge-swift
Use when creating Automerge documents, navigating with ObjId, reading/writing maps/lists/text using the core Document API, working with ScalarValue or Value types, or needing fine-grained control over document mutations. Use this instead of the Codable skill when you need performance (high-frequency updates) or when working with the low-level document structure directly.
How this skill is triggered — by the user, by Claude, or both
Slash command
/automerge-swift:automerge-swift-coreThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- "How do I create an Automerge Document and add a map key?"
The core API gives direct control over the document tree. Every operation takes an ObjId to identify where in the tree you're working.
The document is a tree of nested CRDTs. The root is always a Map at ObjId.ROOT.
Object types (have their own ObjId, can contain children):
| Type | Swift | Create in Map | Create in List |
|---|---|---|---|
| Map | ObjType.Map | putObject(obj:key:ty: .Map) | insertObject(obj:index:ty: .Map) |
| List | ObjType.List | putObject(obj:key:ty: .List) | insertObject(obj:index:ty: .List) |
| Text | ObjType.Text | putObject(obj:key:ty: .Text) | insertObject(obj:index:ty: .Text) |
Scalar types (leaf values, no ObjId):
| Automerge | Swift | ScalarValue |
|---|---|---|
| null | nil | .Null |
| boolean | Bool | .Boolean(_:) |
| unsigned int | UInt | .Uint(_:) |
| signed int | Int | .Int(_:) |
| float | Double | .F64(_:) |
| string | String | .String(_:) — atomic, not concurrent |
| bytes | Data | .Bytes(_:) |
| timestamp | Date | .Timestamp(_:) — Int64 seconds, loses sub-second |
| counter | Counter | .Counter(_:) — concurrent increment |
The Value enum wraps both:
Value.Object(ObjId, ObjType) — nested objectValue.Scalar(ScalarValue) — leaf value// Create empty
let doc = Document()
// Load from bytes
let doc = try Document(savedBytes)
// Save (compacts all changes)
let bytes: Data = doc.save()
// Fork (in-memory copy sharing history)
let fork = doc.fork()
// Fork at a point in history
let oldFork = doc.forkAt(heads: someHeads)
You must create the nested structure before writing values. Every putObject / insertObject returns the new ObjId.
let doc = Document()
// Create a list at root
let itemsId = try doc.putObject(obj: ObjId.ROOT, key: "items", ty: .List)
// Insert a map into the list
let contactId = try doc.insertObject(obj: itemsId, index: 0, ty: .Map)
// Put scalar values into the map
try doc.put(obj: contactId, key: "age", value: .Int(30))
try doc.put(obj: contactId, key: "active", value: .Boolean(true))
// Create a Text object for concurrent string editing
let nameId = try doc.putObject(obj: contactId, key: "name", ty: .Text)
try doc.spliceText(obj: nameId, start: 0, delete: 0, value: "Alice")
// Get single value — returns Value? (can be .Object or .Scalar)
let val = try doc.get(obj: ObjId.ROOT, key: "title")
switch val {
case .Scalar(.String(let s)):
print(s)
case .Object(let id, .Text):
let text = try doc.text(obj: id)
case .Object(let id, .List):
// navigate into list with id
default: break
}
// All keys
let keys = try doc.keys(obj: someMapId)
// All entries as (String, Value) pairs
let entries = try doc.mapEntries(obj: someMapId)
// Size
let count = try doc.length(obj: someMapId)
// Get by index
let val = try doc.get(obj: listId, index: 0)
// All values
let vals = try doc.values(obj: listId)
// Size
let count = try doc.length(obj: listId)
let str = try doc.text(obj: textId)
let len = try doc.length(obj: textId)
For marks, cursors, and full text editing patterns, see /skill automerge-swift-text.
// Set scalar
try doc.put(obj: mapId, key: "name", value: .String("Bob"))
// Set/create nested object (returns new ObjId)
let nestedId = try doc.putObject(obj: mapId, key: "address", ty: .Map)
// Delete key
try doc.delete(obj: mapId, key: "oldField")
// Insert at index
try doc.insert(obj: listId, index: 0, value: .String("first"))
// Insert nested object
let itemId = try doc.insertObject(obj: listId, index: 0, ty: .Map)
// Overwrite at index
try doc.put(obj: listId, index: 0, value: .String("replaced"))
// Delete at index
try doc.delete(obj: listId, index: 2)
// Splice (like Array replaceSubrange)
try doc.splice(obj: listId, start: 1, delete: 2, values: [.String("a"), .String("b")])
try doc.spliceText(obj: textId, start: 5, delete: 3, value: "inserted")
try doc.updateText(obj: textId, value: "new content")
For marks, formatting, and full text editing patterns, see /skill automerge-swift-text.
// Increment counter in map
try doc.increment(obj: mapId, key: "views", by: 1)
// Increment counter in list
try doc.increment(obj: listId, index: 0, by: 1)
// Get object type
let type = try doc.objectType(obj: someId) // .Map, .List, or .Text
// Get path to an object
let path: [PathElement] = try doc.path(obj: someId)
let pathString = path.stringPath() // e.g. ".items[0].name"
// Look up object by string path
let id = try doc.lookupPath(path: ".items[0].name")
This comes up constantly — getting a nested object's ObjId from a parent:
func getListId(from doc: Document, key: String) throws -> ObjId {
guard case .Object(let id, .List) = try doc.get(obj: ObjId.ROOT, key: key) else {
throw MyError.expectedList(key)
}
return id
}
// Usage
let itemsId = try getListId(from: doc, key: "items")
for i in 0..<(try doc.length(obj: itemsId)) {
if case .Object(let contactId, .Map) = try doc.get(obj: itemsId, index: i) {
// work with contactId
}
}
When you need to walk the whole tree:
import AutomergeUtilities
// Get full schema as AutomergeValue tree
let schema = try doc.schema()
// Walk document tree
try doc.walk()
// Check if empty
let empty = try doc.isEmpty()
// Compare two documents
let same = try doc.equivalentContents(otherDoc)
Forgetting to capture ObjId from putObject/insertObject: These methods return the new ObjId. If you discard it, you can't reference the new object.
Using String scalar for user-editable text: Use ObjType.Text with spliceText for anything that will be concurrently edited. String scalars are last-writer-wins.
Indexing after delete: After delete(obj:index:), all subsequent indices shift down. Delete from high to low, or use splice.
Not handling Value.Object vs Value.Scalar: get() returns a Value? which can be either. Always pattern-match both cases.
npx claudepluginhub sitapix/automerge-swift-skills --plugin automerge-swiftReviews SwiftData code for patterns like autosave, relationships, dangerous predicates, CloudKit constraints, indexing, and class inheritance. Use when writing, reviewing, or debugging SwiftData.
Build, review, or improve Core Data persistence for apps not using SwiftData. Covers stack setup, concurrency, batch operations, NSFetchedResultsController, persistent history, staged migration, and testing.
Enforces schema-as-code for Obsidian vaults. Discovers structure, builds a schema, and runs health checks, dedup, link cleanup, MOC generation, and decay cycles.