> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myme.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Data access

> Items, edges, metadata, extensions, blobs, search namespaces

## Namespaces

The client exposes one property per resource, plus top-level `search` and `health`:

* `client.items` — item CRUD, transitions, versions, stats
* `client.metadata` — tags, metadata merge operations
* `client.extensions` — namespaced sidecar data
* `client.edges` — edges and custom edge types
* `client.blobs` — upload, download, URLs
* `client.types` — type schema listing and registration
* `client.keys` — API key management (admin)
* `client.webhooks` — webhook registration and deliveries
* `client.search(query:filters:)` — full-text search
* `client.health()` — server reachability check

### Items

```swift theme={null}
let note = try await client.items.create(
  CreateItemInput(
    type: "core.note",
    properties: ["body": .string("Hello"), "title": .string("First note")],
    tier: .library,
    tags: ["intro"]
  )
)

let fetched = try await client.items.get(id: note.id)

let page = try await client.items.list(
  filters: ListFilters(type: "core.note", state: .active, limit: 50)
)
// page.data: [Item], page.cursor: String?, page.hasMore: Bool

try await client.items.update(
  id: note.id,
  properties: ["body": .string("Updated")],
  options: UpdateOptions(version: note.version)
)

try await client.items.transition(id: note.id, to: "archived")
try await client.items.delete(id: note.id)     // → trashed
try await client.items.restore(id: note.id)    // trashed → active
```

`items.list` returns `PaginatedResult<Item>` with `data`, `cursor`, `hasMore`. Two `AsyncSequence` helpers walk every page automatically — `client.items.all(filters:)` yields `Item`, and `client.items.allWithMetadata(filters:)` yields `ItemWithMetadata` pairs for views that render tags or favorite flags alongside the item.

```swift theme={null}
for try await item in client.items.all(filters: ListFilters(type: "core.note")) {
  // processed page-by-page; no manual cursor handling
}
```

### Edges

```swift theme={null}
let edge = try await client.edges.create(
  source: note.id,
  target: person.id,
  edgeType: "about"
)

let outbound = try await client.edges.listFromSource(sourceId: note.id)
let inbound  = try await client.edges.listToTarget(targetId: person.id, edgeType: "about")

// Global tenant-scoped listing across all items, by edge type.
let allReplies = try await client.edges.list(edgeType: "in-thread")

try await client.edges.delete(id: edge.id)
```

#### Batched backrefs

`edges.listToTargets(targetIds:edgeType:limit:)` resolves inbound edges for many targets in one call:

```swift theme={null}
let backrefs = try await client.edges.listToTargets(
  targetIds: items.map(\.id),
  edgeType: "in-thread"
)
// backrefs: [String: [Edge]] — keyed by target id
backrefs["msg-1"]?.count   // reply count for msg-1
```

In synced and pure-local modes this is one SQL query against the local store. In remote mode it fans out via a bounded `TaskGroup`; cap concurrency via `ClientConfiguration.maxBackrefBatchConcurrency` (default 8). Empty `targetIds` short-circuits to `[:]`, duplicate ids are deduplicated, and unknown ids map to an empty array.

#### Atomic item + edges

```swift theme={null}
let highlight = try await client.items.create(
  CreateItemInput(
    type: "core.highlight",
    properties: [
      "text": .string("important passage"),
      "locator_type": .string("offset"),
      "start_location": .int(123),
      "end_location": .int(189)
    ],
    edges: [
      CreateItemEdge(edgeType: "references", direction: .outbound, otherId: documentId)
    ]
  )
)
```

Item and edges land in one transaction.

#### Custom edge types

Mirrors the TypeScript SDK's `client.edges.types.*`. Admin-gated server-side.

```swift theme={null}
try await client.edges.edgeTypes.create(
  CreateEdgeTypeInput(
    id: "team.commented-on",
    cardinality: .manyToMany,
    sourceTypeConstraints: ["core.note"],
    targetTypeConstraints: ["core.note"]
  )
)

let types = try await client.edges.edgeTypes.list()
try await client.edges.edgeTypes.delete(id: "team.commented-on")
```

#### Bulk writes and chunked iteration

Admin-only bulk surfaces match the [bulk operations](/api/bulk-operations) shape. Pure-local clients iterate through the local store and return per-record outcomes; synced clients enqueue a single replay record; network clients round-trip straight through.

```swift theme={null}
// Items
let items = try await client.items.bulk(BulkInput(items: payload))

// Edges — sibling endpoint for graph-half migrations
let edges = try await client.edges.bulk(BulkEdgeInput(edges: edgePayload))
```

For inputs larger than the server's 5000 per-call cap, the Swift SDK provides client-side chunking helpers — `items.bulkAll` and `edges.bulkAll`. Both clamp `batchSize` to `[1, 5000]`, aggregate per-record outcomes with absolute indices preserved, and accept an optional `progressHandler` for UI wiring.

```swift theme={null}
let result = try await client.edges.bulkAll(
  largeEdgeSet,
  batchSize: 500,
  progressHandler: { completed, total in
    await progressModel.update(completed: completed, total: total)
  }
)
```

Cross-batch atomicity does not hold — each batch's `atomic: true` guarantee stops at its own server transaction. Callers needing strict all-or-nothing semantics across a run larger than one batch reconcile failures out-of-band.

`items.bulkAction(_:)` is the filter-driven sibling of `items.bulk` — instead of taking a list of records, it applies one action across every item matching a `BulkActionFilter`. Use it to archive, trash, restore, purge, retag, flip tier, patch properties, or bump `updated_at` across a server-side slice without fetching ids first.

```swift theme={null}
let result = try await client.items.bulkAction(
  .transition(
    filter: BulkActionFilter(type: "core.note", state: .active, tags: ["stale"]),
    state: .archived,
    options: BulkActionOptions(dryRun: true, maxItems: 1000)
  )
)
// result.matched / .succeeded / .errored / .dryRun
```

`BulkActionInput` is a discriminated enum — `.transition`, `.purge`, `.updateTags`, `.updateLibrary`, `.updateProperties`, `.updateTimestamp`. Purge requires `options.confirm == "PURGE"`. Synced clients queue the action for replay; pure-local clients apply it in a single local-store transaction.

In remote mode, `bulkAction(_:options:)` polls the server-side job until it reaches a terminal status — `completed`, `failed`, or `cancelled`. Surface progress to the UI by passing `BulkActionPollOptions(onProgress:)`:

```swift theme={null}
let result = try await client.items.bulkAction(
  .purge(filter: BulkActionFilter(type: "core.scratch"), options: .init(confirm: "PURGE")),
  options: BulkActionPollOptions(onProgress: { job in
    Task { await viewModel.update(processed: job.processed, of: job.matched) }
  })
)
```

For explicit-control flows (returning a job id to an LLM, driving polling from a long-lived UI), `bulkActionAsync(_:)` POSTs and returns the `BulkActionJob` envelope without polling; `bulkActionStatus(jobId:)` and `bulkActionCancel(jobId:)` wrap the GET / DELETE endpoints. Cancellation throws `BulkJobCancelledError`; a failed job throws `BulkJobFailedError` carrying the worker's reason.

### Metadata

```swift theme={null}
let metadata = try await client.metadata.get(itemId: note.id)
try await client.metadata.addTags(itemId: note.id, tags: ["urgent"])
try await client.metadata.removeTag(itemId: note.id, tag: "urgent")
try await client.metadata.set(itemId: note.id, input: MetadataInput(tags: ["work"]))
try await client.metadata.merge(itemId: note.id, input: MetadataInput(tags: ["q2"]))

// Distinct tags across the tenant, with usage counts.
let tags = try await client.metadata.listTags()
```

`metadata.listTags()` is mode-aware. In synced and pure-local modes the call fetches metadata rows whose parent item is not trashed and buckets the tags in Swift; in remote mode it hits `GET /metadata/tags`. Either way the return shape is `[TagWithCount]` ordered `count DESC, tag ASC`. Local-mode reads short-circuit the network round-trip.

The `tier` field sits on the item itself, not in metadata. Set it on create via `CreateItemInput.tier`, or flip it post-creation via `items.update(id:properties:options:)` with `UpdateOptions(tier: .library)`. Tier-only updates don't conflict — tier is metadata-axis, last-writer-wins. `system.*` items have no tier; the field is optional in the SDK shape (`Tier?`).

#### Consuming per-item metadata in UIs

Two paths, depending on client mode:

* **Synced / pure-local mode.** Apps render lists of items with their tags or favorite flag — a timeline, a grid, a detail view. Use the SDK's reactive layer rather than polling. In Swift, `store.queryItemsWithMetadata(filters:)` returns an `@Observable ItemsWithMetadataQuery` that re-emits `[ItemWithMetadata]` pairs whenever any matching item or metadata row changes. The query reads from the local SQLite store, so no network round-trip per view frame. See [SwiftUI reactive queries](/sdks/swift/sync-engine#swiftui-reactive-queries) for the full list.

* **Remote-only mode.** Fetch pairs in bulk via `client.items.listWithMetadata(filters:)` (wire: `GET /items?include=metadata`). For ad-hoc single-item lookups use `client.metadata.get(itemId:)`.

Apps built against Marfa before the reactive variant existed often hand-rolled a cache that subscribed to the sync engine's events stream and re-ran `listWithMetadata` on each mutation. With `ItemsWithMetadataQuery` available, that bespoke plumbing is redundant — the SDK owns the observation and invalidation.

### Extensions

Namespaced sidecar data. Writes route through the mutation queue in synced mode.

```swift theme={null}
try await client.extensions.set(
  itemId: note.id,
  namespace: "my-app.reading_progress",
  data: ["offset": .int(1234), "percent": .double(45)]
)

let all  = try await client.extensions.getAll(itemId: note.id)
let one  = try await client.extensions.get(itemId: note.id, namespace: "my-app.reading_progress")
try await client.extensions.delete(itemId: note.id, namespace: "my-app.reading_progress")
```

### Blobs

```swift theme={null}
let response = try await client.blobs.upload(data: imageBytes, mimeType: "image/png")
// response.hash, response.url

let (bytes, contentType) = try await client.blobs.download(hash: response.hash)
let exists = try await client.blobs.exists(hash: response.hash)
let url = client.blobs.url(hash: response.hash)
let signed = try await client.blobs.presignedURL(hash: response.hash, ttl: 300)
```

`upload` accepts an optional `onProgress: (@Sendable (Int64, Int64) -> Void)?` for network-only clients that want inline byte-progress updates. The callback fires on the `URLSession` delegate queue with `(bytesSent, totalBytes)` — hop to the main actor yourself if you're driving SwiftUI state from it. Synced-mode clients ignore the callback; progress for queued uploads arrives via [`BlobUploadProgressQuery`](/sdks/swift/sync-engine#blob-upload-progress) on the reactive layer instead.

```swift theme={null}
let response = try await client.blobs.upload(
  data: imageBytes,
  mimeType: "image/png",
  onProgress: { sent, total in
    print("\(sent) / \(total)")
  }
)
```

<Warning>
  Blob operations require a live server. A pure-local client throws `LocalModeUnsupportedError` for every blob method except `url(hash:)`. The same guard applies to `client.types`, `client.keys`, and `client.webhooks`.
</Warning>

### Search

```swift theme={null}
let results = try await client.search(
  query: "orwell",
  filters: SearchFilters(type: "core.bookmark", tier: .library)
)

// Tag filtering — AND semantics, matching ListFilters.tags.
let tagged = try await client.search(
  query: "orwell",
  filters: SearchFilters(tags: ["fiction", "classic"])
)
```

`SearchFilters.tier` is a `TierFilter?` — pass `.library` or `.feed` to narrow, or omit (`nil`) to return both. There's no `.all` synonym; `nil` already expresses that. `SearchFilters.tags` requires items to have every listed tag. `system.*` items are excluded from search by default; opt in with an explicit `type: "system.<X>"` filter.
