---
title: Embed relate in a Swift app
description: "relate, the embedded Cypher graph database (relatedb.ai): A first Swift program on the relate Swift package, which opens a store, passes parameters, runs transactions, reads result metadata, and catches a typed error."
type: tutorial
surfaces:
  - swift
status: stable
verified: 0.1.2
verified_by:
  - swift
samples:
  - https://relatedb.ai/docs/get-started/swift/samples/version.json
  - https://relatedb.ai/docs/get-started/swift/samples/open.json
  - https://relatedb.ai/docs/get-started/swift/samples/create.json
  - https://relatedb.ai/docs/get-started/swift/samples/params.json
  - https://relatedb.ai/docs/get-started/swift/samples/transaction.json
  - https://relatedb.ai/docs/get-started/swift/samples/metadata.json
  - https://relatedb.ai/docs/get-started/swift/samples/error.json
  - https://relatedb.ai/docs/get-started/swift/samples/program.json
canonical_url: https://relatedb.ai/docs/get-started/swift/
prerequisites: []
related:
  - get-started/python
  - get-started/rust
  - get-started/java
  - get-started/go
  - reference/errors
  - concepts/transactions-and-concurrency
  - reference/storage/formats
  - reference/api/swift
  - guides/embed/swift
  - concepts/problems-relate-solves#embedded-graph-in-an-app
last_updated: 2026-11-09
---

# Embed relate in a Swift app

A first Swift program on the relate Swift package, which opens a store, passes parameters, runs transactions, reads result metadata, and catches a typed error.

> Verified against relate 0.1.2. If your `relate --version` is newer, read https://relatedb.ai/docs/reference/changelog/#since-0.1.2

By the end of this page, you'll have a Swift program that keeps a small graph of people and projects in one file. It reads the graph with parameters and reports errors by their case and code. Along the way we write inside a [transaction](https://relatedb.ai/docs/reference/glossary/#transaction), roll one back, and read what a result says about itself. It takes about fifteen minutes. relate runs inside your app's process, so there is no server to start, and every step writes to a store it creates.

## Prerequisites

- Swift 6.0 or later on macOS 13 or later. The package also builds for iOS 16 or later; this page runs on the Mac.
- A terminal and a text editor. The relate CLI is not needed: the package carries the engine.

## Install the package

The Swift [client](https://relatedb.ai/docs/reference/glossary/#client) is the SwiftPM package `Relate`, imported as `import Relate`. Create an executable package and add it:

```bash
mkdir Team && cd Team
swift package init --type executable
```

In `Package.swift`, add the package to `dependencies` and the product to your executable target's `dependencies`:

```swift
.package(url: "https://github.com/relatedb/relate", from: "0.1.2")
.product(name: "Relate", package: "relate")
```

The package declares iOS 16 and macOS 13 as its minimum platforms, so set `platforms: [.macOS(.v13)]` on your package too. It links the engine as a prebuilt static XCFramework, so the engine is not compiled from source; only the package's Swift wrapper is. Each program on this page is top-level code in `Sources/Team/main.swift`, run with `swift run`. Top-level code needs that file to be the target's only entry point, so remove any file with `@main` that the template created in `Sources/Team`. The package has no version constant of its own, so this program asks the engine which release it is:

```swift run name=version
import Relate

let db = try Database(path: ":memory:")
let conn = try db.connect()
let result = try conn.query(
    "CALL dbms.components() YIELD name, versions "
        + "WHERE name = 'Relate' RETURN versions[0] AS version")
print(result.rows[0][0].stringValue ?? "unknown")
```

```text
0.1.2
```

Notice the version: the programs on this page were checked against that release, and their recorded output comes from it.

## Open a store

A [store](https://relatedb.ai/docs/reference/glossary/#store) is where relate keeps a graph. `:memory:` is a store that lives in the process and forgets everything when it closes. A path ending in `.rl` is a [single-file store](https://relatedb.ai/docs/reference/glossary/#single-file-store), which `Database(path:)` creates when nothing is there yet.

```swift run name=open
import Foundation
import Relate

let memory = try Database(path: ":memory:")
let conn = try memory.connect()
let result = try conn.query("RETURN 'hello' AS greeting")
print(result.rows[0][0].stringValue ?? "")

_ = try Database(path: "./team.rl")
print(FileManager.default.fileExists(atPath: "team.rl"))
```

```text
hello
true
```

`Database(path:)` opens a store, `connect()` returns a connection on it, and `query(_:_:)` runs one statement and returns its whole result. Every call is synchronous and throws on failure, so each one takes `try`. A `Database` closes when its last reference is released, and each connection holds its database. Notice that `team.rl` now exists.

## Create the graph

Our graph is three people and two projects: who works on what, since when, and who maintains `relate`. One `CREATE` statement writes all of it. The rest of this page uses the same statement, as `team`, on an in-memory store, so each program runs on its own.

```swift run name=create
import Relate

let team = """
    CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
           (grace:Person {name: 'Grace', role: 'engineer'}),
           (noor:Person {name: 'Noor', role: 'maintainer'}),
           (relate:Project {name: 'relate', language: 'Rust'}),
           (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
           (ada)-[:WORKS_ON {since: 2024}]->(relate),
           (grace)-[:WORKS_ON {since: 2023}]->(relate),
           (grace)-[:WORKS_ON {since: 2022}]->(tck),
           (noor)-[:MAINTAINS]->(relate),
           (ada)-[:KNOWS]->(grace)
    """

let db = try Database(path: ":memory:")
let conn = try db.connect()
let result = try conn.query(team)
print("nodes created:", result.stats.nodesCreated)
print("relationships created:", result.stats.relationshipsCreated)
```

```text
nodes created: 5
relationships created: 5
```

A write returns no rows, so we read its counters from `result.stats`. Five nodes and five relationships: the whole team.

## Read it back with parameters

A statement names each value it needs as `$name`, and `query(_:_:)` takes the values as a `[String: CypherValue]` dictionary. The statement text stays the same while the values change, and a value never becomes part of the Cypher.

```swift run name=params
import Relate

let team = """
    CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
           (grace:Person {name: 'Grace', role: 'engineer'}),
           (noor:Person {name: 'Noor', role: 'maintainer'}),
           (relate:Project {name: 'relate', language: 'Rust'}),
           (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
           (ada)-[:WORKS_ON {since: 2024}]->(relate),
           (grace)-[:WORKS_ON {since: 2023}]->(relate),
           (grace)-[:WORKS_ON {since: 2022}]->(tck),
           (noor)-[:MAINTAINS]->(relate),
           (ada)-[:KNOWS]->(grace)
    """

let teamOf = """
    MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project})
    RETURN p.name AS name, w.since AS since
    ORDER BY since
    """

let db = try Database(path: ":memory:")
let conn = try db.connect()
try conn.query(team)
for project in ["relate", "opencypher-tck"] {
    print(project)
    for row in try conn.query(teamOf, ["project": CypherValue(project)]).records {
        let name = row["name"]?.stringValue ?? ""
        let since = row["since"]?.intValue ?? 0
        print("  \(name) since \(since)")
    }
}
```

```text
relate
  Grace since 2023
  Ada since 2024
opencypher-tck
  Grace since 2022
```

`records` gives each row as a dictionary keyed by the names in `RETURN`. Each value is a `CypherValue`, and `stringValue` and `intValue` read it as a Swift type, or `nil` when it is another kind. A literal needs no wrapping (`["project": "relate"]`), and a variable goes through `CypherValue(_:)`. Notice that Noor isn't listed: she maintains `relate` through a `MAINTAINS` relationship, and the pattern asks for `WORKS_ON`.

## Change it in a transaction

A connection commits each statement on its own until you call `begin()`. From then on, every statement is part of one transaction, `commit()` keeps it, and `rollback()` throws it away. This program adds Lin to the team and commits, then deletes every project and rolls back.

```swift run name=transaction
import Relate

let team = """
    CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
           (grace:Person {name: 'Grace', role: 'engineer'}),
           (noor:Person {name: 'Noor', role: 'maintainer'}),
           (relate:Project {name: 'relate', language: 'Rust'}),
           (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
           (ada)-[:WORKS_ON {since: 2024}]->(relate),
           (grace)-[:WORKS_ON {since: 2023}]->(relate),
           (grace)-[:WORKS_ON {since: 2022}]->(tck),
           (noor)-[:MAINTAINS]->(relate),
           (ada)-[:KNOWS]->(grace)
    """

func count(_ conn: Connection, _ label: String) throws -> Int64 {
    try conn.query("MATCH (n:\(label)) RETURN count(n) AS n").scalar().intValue ?? 0
}

let db = try Database(path: ":memory:")
let conn = try db.connect()
try conn.query(team)

try conn.begin()
try conn.query(
    "MATCH (tck:Project {name: 'opencypher-tck'}) "
        + "CREATE (:Person {name: 'Lin', role: 'engineer'})-[:WORKS_ON {since: 2026}]->(tck)")
let bookmark = try conn.commit()
print("committed:", bookmark != nil)
print("people after commit:", try count(conn, "Person"))

try conn.begin()
try conn.query("MATCH (p:Project) DETACH DELETE p")
print("projects inside the transaction:", try count(conn, "Project"))
try conn.rollback()
print("projects after rollback:", try count(conn, "Project"))
```

```text
committed: true
people after commit: 4
projects inside the transaction: 0
projects after rollback: 2
```

Inside the transaction, the delete is visible: zero projects. After `rollback()`, both are back, and Lin, who was committed, stays. `commit()` returns a [bookmark](https://relatedb.ai/docs/reference/glossary/#bookmark), the commit's place in the store's history as `relate:<n>`, or `nil` when the transaction wrote nothing. `scalar()` reads a result of exactly one row and one column. Each transaction starts with its own `begin()`.

## Read result metadata

A `QueryResult` describes itself as well as holding rows: the column names, what kind of statement it was, and a `stats` value that counts what the statement changed.

```swift run name=metadata
import Relate

let team = """
    CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
           (grace:Person {name: 'Grace', role: 'engineer'}),
           (noor:Person {name: 'Noor', role: 'maintainer'}),
           (relate:Project {name: 'relate', language: 'Rust'}),
           (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
           (ada)-[:WORKS_ON {since: 2024}]->(relate),
           (grace)-[:WORKS_ON {since: 2023}]->(relate),
           (grace)-[:WORKS_ON {since: 2022}]->(tck),
           (noor)-[:MAINTAINS]->(relate),
           (ada)-[:KNOWS]->(grace)
    """

let db = try Database(path: ":memory:")
let conn = try db.connect()
try conn.query(team)

let result = try conn.query(
    "MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project}) "
        + "RETURN p.name AS name, w.since AS since ORDER BY since",
    ["project": "relate"])
print(result.columns)
for (column, value) in zip(result.columns, result.rows[0]) {
    print(column, value.kind)
}
print("query type:", result.queryType.rawValue)

let write = try conn.query("MATCH (n:Person {name: 'Noor'}) SET n.role = 'lead'")
print("query type:", write.queryType.rawValue)
print("properties set:", write.stats.propertiesSet)
```

```text
["name", "since"]
name string
since int
query type: r
query type: w
properties set: 1
```

A result has no list of column types: each value carries its own kind, which `kind` reads back, and a node arrives as `.node` with its labels and properties. `queryType` is a `QueryType` whose raw value is `r` for a read and `w` for a write that returns no columns. `stats` holds all 11 change counters, zeros included.

## Catch an error

Every failure throws a `RelateError`, and its case says where it came from. A statement the engine rejects is `.engine`, carrying an `EngineError`. A statement that uses `$name` without a value is rejected before it runs:

```swift run name=error
import Relate

let team = """
    CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
           (grace:Person {name: 'Grace', role: 'engineer'}),
           (noor:Person {name: 'Noor', role: 'maintainer'}),
           (relate:Project {name: 'relate', language: 'Rust'}),
           (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
           (ada)-[:WORKS_ON {since: 2024}]->(relate),
           (grace)-[:WORKS_ON {since: 2023}]->(relate),
           (grace)-[:WORKS_ON {since: 2022}]->(tck),
           (noor)-[:MAINTAINS]->(relate),
           (ada)-[:KNOWS]->(grace)
    """

let roleOf = "MATCH (p:Person {name: $name}) RETURN p.role AS role"

let db = try Database(path: ":memory:")
let conn = try db.connect()
try conn.query(team)

do {
    try conn.query(roleOf)
} catch RelateError.engine(let e) {
    print(e.message)
    print("gqlStatus:", e.gqlStatus)
    print("code:", e.code)
    print("stage:", e.stage)
    print("retryable:", e.retryable)
}

print(try conn.query(roleOf, ["name": "Noor"]).scalar().stringValue ?? "")
```

```text
semantic error: ParameterMissing: Expected parameter(s): name
gqlStatus: 42N02
code: Neo.ClientError.Statement.ParameterMissing
stage: compile
retryable: false
maintainer
```

The message says what's missing; the properties are what code branches on:

| Property    | This error                                   | Meaning                                                                                                                                                  |
| ----------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gqlStatus` | `42N02`                                      | The GQLSTATUS code; the Python and Node.js clients report the same code for the same error                                                               |
| `code`      | `Neo.ClientError.Statement.ParameterMissing` | The Neo4j status code for the same error                                                                                                                 |
| `stage`     | `compile`                                    | Where it was found: `.compile` before any work, `.runtime` while producing rows, `.internal` for a failure outside the statement, such as a locked store |
| `retryable` | `false`                                      | Whether running it again can succeed; `true` for a write-write conflict, never for a mistake in the statement                                            |

The other two cases come from the client, not the engine: `.invalidParameter` for a value it refuses to bind, such as a node, and `.decoding` for a reply that breaks the engine's contract. The [errors reference](https://relatedb.ai/docs/reference/errors/#engine-parameter-missing) lists this message with its cause and remedy. With the value bound, the same statement finds Noor's role.

## Put it together

`main.swift` puts the steps together in one program. It opens `team.rl`, creates the graph only when the store has no people yet, and prints who works on each project. A relate error anywhere ends it with the error's code and a nonzero exit status.

```swift run name=program title="main.swift"
import Foundation
import Relate

let team = """
    CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
           (grace:Person {name: 'Grace', role: 'engineer'}),
           (noor:Person {name: 'Noor', role: 'maintainer'}),
           (relate:Project {name: 'relate', language: 'Rust'}),
           (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
           (ada)-[:WORKS_ON {since: 2024}]->(relate),
           (grace)-[:WORKS_ON {since: 2023}]->(relate),
           (grace)-[:WORKS_ON {since: 2022}]->(tck),
           (noor)-[:MAINTAINS]->(relate),
           (ada)-[:KNOWS]->(grace)
    """

let teamOf = """
    MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project})
    RETURN p.name AS name, w.since AS since
    ORDER BY since
    """

func ensureTeam(_ conn: Connection) throws {
    let people = try conn.query("MATCH (p:Person) RETURN count(p) AS n").scalar().intValue ?? 0
    if people == 0 {
        try conn.query(team)
        print("created the team graph")
    } else {
        print("found \(people) people")
    }
}

func report(_ conn: Connection, _ project: String) throws {
    print(project)
    for row in try conn.query(teamOf, ["project": CypherValue(project)]).records {
        print("  \(row["name"]?.stringValue ?? "") since \(row["since"]?.intValue ?? 0)")
    }
}

func run() throws {
    let db = try Database(path: "./team.rl")
    let conn = try db.connect()
    try ensureTeam(conn)
    try report(conn, "relate")
    try report(conn, "opencypher-tck")
}

do {
    try run()
} catch let error as RelateError {
    let code = error.engineError?.gqlStatus ?? "client"
    FileHandle.standardError.write(Data("relate error \(code): \(error.message)\n".utf8))
    exit(1)
}
```

```text
created the team graph
relate
  Grace since 2023
  Ada since 2024
opencypher-tck
  Grace since 2022
```

Run it with `swift run`. Each statement is durable in `team.rl` when `query(_:_:)` returns, and the store closes when `run()` returns and releases the last reference to it. Run it a second time and it prints `found 3 people` instead of creating the graph again, then the same report. If another process has `team.rl` open for writing, `Database(path:)` throws `.engine` with stage `.internal`, and the program prints `relate error 42000: internal error` and exits with status 1.

To read the file with no chance of changing it, open it read-only: `Database(readOnlyPath: "./team.rl")`. A write then throws `.engine` with GQLSTATUS `42N18`, and a path with no store behind it throws instead of creating a new store.

## What you built

You have a Swift program that keeps a graph of people and projects in `team.rl`, one file you can copy or archive. It reads with parameters, writes inside transactions, knows a read from a write by its query type, and reports errors by case and GQLSTATUS code. Ada, Grace, and Noor are waiting in that file for the next run.

## Next

- [Embed relate in a Python app](https://relatedb.ai/docs/get-started/python/): the same steps, the same graph, in Python.
- [Use the Swift package](https://relatedb.ai/docs/guides/embed/swift/): open modes, parameters, transactions, metadata and errors in depth.
- [Swift API](https://relatedb.ai/docs/reference/api/swift/): every type and method.
- [Errors](https://relatedb.ai/docs/reference/errors/): every error message, its cause, its remedy, and its codes.
- [Transactions and concurrency](https://relatedb.ai/docs/concepts/transactions-and-concurrency/): what a transaction isolates, and why a store has one writer.

## Terms used on this page

- **transaction**: A unit of work that commits as a whole or not at all; each statement is its own transaction unless an explicit `begin` … `commit` groups several.
- **client**: A language package that runs the relate engine inside a program's process: the Python, Node.js, Rust, Java, Go and Swift clients.
- **store**: A relate database: a directory, a single `.rl` file, or `:memory:`, opened with `--database`.
- **single-file store**: A store kept in one `.rl` file that holds the snapshot and the WAL together; the layout for a `--database` path ending in `.rl`.
- **bookmark**: The marker a commit returns, `relate:<n>`, naming its place in the store's commit sequence; returned by the clients and over Bolt.
