Skip to content
Go To Dashboard

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.

What the agent needsRecommended operation
A new Postgres databasedatabase.create({})
A stable key for a database reused by later steps or runsCreate with handle, then database.get(handle)
Databases the tenant already holdsdatabase.list()
Removing a database and freeing its plan slotdatabase.delete(idOrHandle)
Queries, migrations, or transactionsA 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.

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.

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.

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.