Data
Data lets a deployed agent provision a permanent PostgreSQL database and use it through a standard Postgres client. Agent steps call the typed ctx.sapiom.database capability; Sapiom supplies the authenticated cloud connection when the deployed agent runs.
This is a provisioning capability, not a SQL query API. database.create returns connection credentials; the agent project supplies the database client and runs its own SQL.
Choose the database operation
Section titled “Choose the database operation”| What the agent needs | Recommended operation |
|---|---|
| A new Postgres database | database.create({}) |
| A stable key for a database reused by later steps or runs | Create with handle, then database.get(handle) |
| Databases the tenant already holds | database.list() |
| Removing a database and freeing its plan slot | database.delete(idOrHandle) |
| Queries, migrations, or transactions | A standard Postgres client using connectionString |
A database is permanent: it lives until you delete it. There is no lifetime to choose, and every create field is optional. Each database holds one slot of the plan’s database limit from create until delete. create still accepts the deprecated duration field so older code compiles, but it ignores the value; omit it.
Provision, query, and delete a scratch database
Section titled “Provision, query, and delete a scratch database”Delete a database the agent needs for only one run when the run finishes, because nothing else removes it. This example assumes the agent project includes pg:
import { Client } from "pg";
const db = await ctx.sapiom.database.create({ name: "report-build",});
if (db.status !== "active" || !db.connection) { throw new Error(`Database ${db.id} is ${db.status}`);}
const client = new Client({ connectionString: db.connection.connectionString,});
try { await client.connect(); await client.query( "create table report_rows (label text not null, value integer not null)", ); await client.query( "insert into report_rows (label, value) values ($1, $2)", [input.label, input.value], );
const result = await client.query( "select label, value from report_rows order by label", ); return { rows: result.rows };} finally { await client.end().catch(() => {}); await ctx.sapiom.database.delete(db.id).catch((error) => { ctx.logger.warn("database cleanup failed", { databaseId: db.id, error: String(error), }); });}Use parameterized queries for values. Never construct SQL by interpolating caller input, and never return or log connectionString, username, or password.
Reuse a database deliberately
Section titled “Reuse a database deliberately”A handle is a tenant-scoped, human-friendly lookup key. Use one when another step or a later run must find the same database. Before creating a database with a handle, check whether the tenant already holds it (get(handle) or list()), so repeated runs reuse one database instead of each holding a new plan slot. A later step reads it back by handle:
const db = await ctx.sapiom.database.get(input.databaseHandle);
if (db.status !== "active" || !db.connection) { throw new Error(`Database ${input.databaseHandle} is ${db.status}`);}Pass the handle or database ID between steps, not the connection credentials. A database can be provisioning with connection: null, and another step or run can delete it between lookup and connection, so handle both conditions. get fails when the handle was never created or has been deleted.
Test the behavior locally
Section titled “Test the behavior locally”Local Run replaces ctx.sapiom.database calls with deterministic records. It does not provision Postgres. The built-in connection points at fixture localhost credentials; a direct pg client remains real and can attempt a local connection.
Test provisioning and lifecycle branches with exact capability fixtures, and put SQL access behind an injectable adapter when the Local Run must exercise query-dependent logic:
{ "version": 1, "steps": { "prepare-data": { "database.create": { "id": "db-local", "handle": null, "name": "report-build", "description": null, "status": "active", "region": "us-east-1", "pgVersion": 17, "connection": { "connectionString": "postgresql://fixture.invalid/report" }, "createdAt": "2098-12-31T23:00:00.000Z" } } }}The override is returned verbatim. Supply the shape the step consumes, assert the terminal output, and require both unusedStubs and stubWarnings to be empty. Use a small production run to verify real provisioning, client connectivity, SQL behavior, and cleanup.
Manage lifecycle and usage
Section titled “Manage lifecycle and usage”Nothing deletes a database automatically. A database stays active, and keeps its plan slot, until a delete call removes it. Delete scratch databases when the work finishes, use list() to find databases left by an interrupted run, and make retry behavior explicit when a stable handle might already exist.
Usage is metered by count: each database held counts against the plan’s database limit for as long as it exists. Databases created before the lifetime was retired may still report duration and expiresAt; nothing enforces either value, so do not build logic on them. Use the signed-in capability catalog for current regions, per-database limits, and pricing; do not copy a rate or limit into agent logic.
© 2026 Sapiom, Inc.