Database
A Swirls-managed PostgreSQL database, declared with a Prisma schema, migrated automatically, and queried from workflows through a generated typed client.
What it is. A PostgreSQL database Swirls provisions and operates for your project, declared with a Prisma schema instead of a connection string.
Use it when you want a relational database without provisioning one yourself, and you want to query it from workflows with a typed client instead of hand-written SQL.
Works with type: database nodes (governed, reviewable, traced mutations) and context.db.<name> inside code node @ts blocks (the full client); migration blocks declare data transforms a schema change can't express. For a database you already run yourself, use Postgres instead.
database blocks declare a Swirls-managed Postgres: Swirls provisions it on deploy, migrates its schema when it changes, and holds the connection encrypted with your project's keyset. You never see or manage the connection string. Query the database from @ts blocks through a generated, fully typed Prisma client, so you write context.db.my_db.user.findMany({ where: { role: "ADMIN" } }) instead of raw SQL and a matching row schema.
Managed vs. external
database | postgres | |
|---|---|---|
| Ownership | Swirls provisions and operates it | You supply a connection string to a database you run |
| Schema | Prisma schema language, migrated as real DDL | Hand-written JSON Schema per table, validation only |
| Queries | Generated typed Prisma client (context.db.<name>) | Raw @sql with {{key}} placeholders |
| Connection | Held by Swirls, encrypted with your project's keyset | A secret reference or literal you provide |
Use database for a new relational store you want Swirls to run. Use postgres for a database you already operate.
Declaring a database block
There is no type: field: the keyword database identifies the block. Its schema is Prisma schema language inside a @prisma { } island.
| Field | Required | Description |
|---|---|---|
label | No | Human-readable label. |
description | No | Human-readable description. |
schema | Yes | @prisma { } island: models and enums, written in the Prisma schema language. |
database my_db {
label: "App database"
schema: @prisma {
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
email String @unique
role Role @default(USER)
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
enum Role {
USER
ADMIN
}
}
}The @prisma island holds models and enums only. Don't add a datasource or generator block, and never write a connection URL: Swirls wraps your schema with its own datasource and generator before validating it, so a user-supplied one collides and fails validation. On deploy, Swirls provisions the database if it doesn't exist yet, then converges its schema to match what you declared.
Querying: context.db.<name>
Inside a code node's @ts block, context.db.<name> is the full generated Prisma client for a declared database block: every model, every query method, no SQL to write and no schema to keep in sync by hand.
node active_admins {
type: code
label: "Active admins"
code: @ts {
return await context.db.my_db.user.findMany({
where: { role: "ADMIN" },
include: { posts: true },
})
}
}return await ... works directly in @ts bodies: a returned promise resolves before it becomes the node's output.
Data round-trips with real types: DateTime fields come back as JavaScript Date objects, BigInt and byte columns round-trip as BigInt and bytes, and Decimal values come back as strings.
$transaction is not available in a code node. The client here is for straightforward reads and writes. A multi-step atomic transaction needs the governed database node's operation: transaction, below.
Governed mutations: the database node
A code node's client is unrestricted and invisible to reviews, policy, and traces — nothing marks a deleteMany buried inside it as a delete. The database node is an opt-in surface for a mutation you want visible in flow { }, gateable with review:, and traced as its own step.
| Field | Required | Description |
|---|---|---|
database | Yes | Bare identifier naming a top-level database block. |
operation | Yes | One of query, insert, update, delete, transaction. Narrows which client methods run can call. |
condition | No | @ts block returning a boolean; if false, the node is skipped. |
run | Yes | @ts block: the typed Prisma body. |
The declared operation mints a capability-narrowed client. A call outside that capability is rejected at runtime, not just flagged by a type:
operation | Client exposes |
|---|---|
query | findMany, findFirst, findUnique, count, aggregate, groupBy (and the *OrThrow read variants) |
insert | create, createMany |
update | update, updateMany, upsert |
delete | delete, deleteMany |
transaction | The full client, inside one atomic $transaction |
node purge_stale {
type: database
label: "Purge stale users"
database: my_db
operation: delete
review: { enabled: true }
condition: @ts {
return context.nodes.root.output.confirmed === true
}
run: @ts {
return context.db.my_db.user.deleteMany({
where: { lastSeen: { lt: context.nodes.root.output.cutoff } },
})
}
}operation: transaction is the exception: its run body gets the full client, opened inside $transaction, for the atomic multi-step case a single narrowed operation can't express.
node settle_invoice {
type: database
label: "Settle invoice"
database: my_db
operation: transaction
review: { enabled: true }
run: @ts {
return context.db.my_db.$transaction(async (tx) => {
const invoice = await tx.invoice.update({
where: { id: context.nodes.root.output.invoiceId },
data: { status: "PAID" },
})
await tx.ledgerEntry.create({
data: { invoiceId: invoice.id, amount: invoice.total },
})
return invoice
})
}
}Because a transaction node spans every operation class, it's governed at the node grain rather than per method: review: and traces treat the whole transaction as one step.
Output is whatever run returns. When condition is false, the node is skipped and its output is { skipped: true }.
Schema migrations
When the @prisma schema changes, the next deploy migrates the managed database to match it:
- Additive changes apply automatically. Adding a model, a field, or an index doesn't require approval.
- Destructive or unclassifiable changes are gated. Dropping a column, changing a type, or any change Swirls can't classify as safe is held for approval rather than applied silently. You approve a gated migration from the CLI or the cloud dashboard before it runs.
Data transforms: the migration block
A schema diff can express "add a column," but not "collapse first_name and last_name into name." A migration block declares that kind of data transform, and runs it after its schema migration, in order, exactly once.
| Field | Required | Description |
|---|---|---|
database | Yes | Bare identifier naming the target database block. |
order | Yes | Non-negative integer. Migrations for the same database run in ascending order. |
operation | Yes | @ts block: the typed Prisma data-migration body. |
migration collapse_names {
database: my_db
order: 1
operation: @ts {
const users = await context.db.my_db.user.findMany({
where: { name: null },
})
for (const user of users) {
await context.db.my_db.user.update({
where: { id: user.id },
data: { name: `${user.firstName} ${user.lastName}` },
})
}
}
}The operation body uses the same client as a code node: model methods like findMany and update, awaited one call at a time. A pending data migration gates its deploy the same way a destructive schema change does, until approved.
Browsing data
The cloud dashboard includes a read-only table and row browser for each managed database, so you can inspect data without leaving Swirls.
Choosing between memory primitives
- Streams: structured workflow output you want to query and reuse. Swirls-managed.
- Disks: unstructured files and shared working space. You control the layout.
- Database: a relational store Swirls provisions and migrates for you.
- Postgres: the relational database you already have.
Further reading
- Node types: the
databasenode's fields - Context:
context.dbin@tsblocks - Reviews: gating a
databasenode withreview: