A2A Agent
A thinking PostgreSQL agent for AI agents and developers. Describe what you want — the agent plans and executes all the steps internally. No orchestration from your side.
Just send text — agent handles the rest
A2A v0.2 spec or flat — same agent# One request. Agent provisions DB, creates schema, seeds data,
# generates TypeScript types, and answers your question.
curl -s -X POST https://api.dbaas.dev/a2a/tasks/send \
-H 'Content-Type: application/json' \
-d '{
"id": "x",
"message": {"role":"user","parts":[{"text":
"create a blog DB with users, posts, comments. seed 10 rows.
generate TypeScript types. how many posts per user?"
}]}
}' | jq .task_idReturns task_id in ~100ms. Poll until state=completed — result contains every step's output including connection string and generated code. Spec-compliant JSON-RPC equivalent below ↓
Two wire formats — both accepted
A2A v0.2 specThe same agent answers both shapes. Spec-compliant clients use JSON-RPC 2.0 at POST /a2a. Custom REST clients keep using the flat POST /a2a/tasks/send shape — unchanged. The wire format dictates response shape, not the URL.
POST /a2acurl -X POST https://api.dbaas.dev/a2a \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tasks/send",
"params": {
"id": "client-1",
"message": {"role":"user","parts":[
{"text":"create a blog DB and seed it"}
]}
}
}'
# 202 + JSON-RPC envelope:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"id": "<task-uuid>",
"status": {"state":"submitted"}
}
}
# Poll via tasks/get (same envelope):
curl -X POST https://api.dbaas.dev/a2a \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tasks/get",
"params":{"id":"<task-uuid>"}}'POST /a2a/tasks/sendcurl -X POST https://api.dbaas.dev/a2a/tasks/send \
-H 'Content-Type: application/json' \
-d '{
"id": "client-1",
"message": {
"role": "user",
"parts": [{"text":"create a blog DB and seed it"}]
}
}'
# 202 + flat shape (unchanged):
{
"task_id": "<task-uuid>",
"skill": "workflow",
"status": {"state":"submitted"},
"poll_url": "/a2a/tasks/<task-uuid>",
"created_at": "2026-..."
}
# Poll via REST GET (unchanged):
curl https://api.dbaas.dev/a2a/tasks/<task-uuid>tasks/send · tasks/get. tasks/cancel + tasks/sendSubscribe return -32601 (not yet supported).
-32700 parse · -32600 invalid request · -32601 method not found · -32602 invalid params · -32603 internal · -32001 task not found.
The JSON-RPC id (string · number · null) is echoed back verbatim — type preserved.
How it works
A2A (Agent-to-Agent) is a protocol for AI agents to call other AI agents over HTTP. dbaas.dev is a thinking agent — it holds the plan, sequences skills internally, and returns a complete result. You don't orchestrate individual steps.
A2A v0.2 card. Advertises protocol, protocolVersion, capabilities (incl. stateTransitionHistory), authentication.schemes:["Bearer"], interfaces[] (json-rpc + rest-legacy), defaultSkill: "workflow", full limits object, and skills with JSON schemas.
Returns 202 + task id instantly. JSON-RPC at /a2a, flat shape at /a2a/tasks/send. Work runs in a detached goroutine.
submitted → working → completed | failed. Result includes all step outputs. Credentials on first poll only.
Your agent dbaas.dev A2A Internal planner
────────── ───────────────── ────────────────
POST /a2a (json-rpc) OR
POST /a2a/tasks/send (flat) ──→ 202 + task_id ──→ LLM plans:
"create blog DB { state: submitted } provision →
seed 10 rows create-schema →
give me types" { state: working } load-sample-data →
generate-orm-models
{ state: completed }
POST /a2a tasks/get OR ──→ result: {
GET /a2a/tasks/{id} ──→ connection_string,
tables, code, ... }Try it in 30 seconds
no API keyflat shapeOne plain-text request. Flat shape below — for the JSON-RPC equivalent see the "Two wire formats" section above. Both produce identical work.
- 1 · Submit (no skill needed)
TASK=$(curl -s -X POST https://api.dbaas.dev/a2a/tasks/send \ -H 'Content-Type: application/json' \ -d '{ "id":"client-1", "message":{"role":"user","parts":[{ "text":"create a database for a blog with users, posts, comments and seed 10 sample rows" }]} }' | jq -r .task_id) echo "task: $TASK" - 2 · Poll until completed (~2–3 min)
while true; do R=$(curl -s https://api.dbaas.dev/a2a/tasks/$TASK) S=$(echo "$R" | jq -r .status.state) echo "[$(date +%H:%M:%S)] $S" [ "$S" = "completed" ] || [ "$S" = "failed" ] && echo "$R" | jq && break sleep 5 done
- 3 · Connect (credentials on first poll only)
psql "postgresql://user:[email protected]:port/db?sslmode=require" \dt # 3 tables: users, posts, comments SELECT count(*) FROM posts; # 10
Skills
workflow is the default — send plain text, agent picks and sequences skills internally. Direct-access skills are available when you need deterministic, single-step control: send metadata.skill: <id> (flat) or params.message.metadata.skill: <id> (JSON-RPC). Same skill IDs in either shape.
Primary interface
workflowDEFAULT — send any goal as plain text. Agent plans + executes all steps internally. No skill selection needed.
Lifecycle (direct)
provision-postgresSpin up a 1–60 minute Postgres database
get-statusCheck DB status + remaining TTL + connection string
extend-ttlBump TTL on a running DB (capped at 90 min total by default)
delete-dbTear down a DB immediately (frees IP slot)
list-my-dbsList all DBs owned by your token/IP
SQL (direct)
exec-sqlRun any SQL (filter rejects COPY + dangerous fns)
generate-queryNL → SQL. Optional execute=true to run it
chat-with-dataNL question → SELECT → execute → NL answer
Schema (direct)
describe-schemaTables + columns + indexes + FKs as JSON
create-schemaGenerate full schema from NL description
migrate-schemaIncremental ALTER from NL change request
reset-schemaDROP all tables (CASCADE) — requires confirm=true
Data (direct)
load-sample-dataGenerate + insert realistic sample data
export-dataExport rows as JSON (capped per table)
Code (direct)
add-procedureGenerate + install a PL/pgSQL stored procedure
generate-orm-modelsSchema → TS / Pydantic / SQLAlchemy / Prisma / Go / Rust
Common patterns
Wrap the flat body in params and add the JSON-RPC envelope. Skill metadata, message parts, ownership token — all stay in the same place inside params.message.
# Flat (POST /a2a/tasks/send):
{ "id": "x", "message": { ... } }
# JSON-RPC equivalent (POST /a2a):
{ "jsonrpc": "2.0", "id": 1, "method": "tasks/send",
"params": { "id": "x", "message": { ... } } }
# Poll equivalent:
GET /a2a/tasks/<task-uuid> ←→ POST /a2a {"jsonrpc":"2.0","id":2,
"method":"tasks/get",
"params":{"id":"<task-uuid>"}}Describe the full goal. Agent plans and runs all steps.
{
"id": "client-1",
"message": {
"role": "user",
"parts": [{"text":
"create a credit scoring database, fill it
with 5 sample applicants, add a stored
procedure that calculates credit score
from income and debts, then generate
Pydantic models for FastAPI"
}]
}
}{
"jsonrpc": "2.0",
"id": 1,
"method": "tasks/send",
"params": {
"id": "client-1",
"message": {
"role": "user",
"parts": [{"text":
"create a credit scoring database, fill it
with 5 sample applicants, add a stored
procedure ... generate Pydantic models"
}]
}
}
}Agent plans: provision → create-schema → load-sample-data → add-procedure → generate-orm-models. Single submit. Poll for full result.
Pass metadata.database_id to operate on a DB you already provisioned. The agent introspects the current schema and plans accordingly — no fresh DB created.
# Migrate:
{
"id": "t1",
"message": {
"metadata": { "database_id": "<id>" },
"parts": [{"text":
"add a deleted_at TIMESTAMPTZ column
to users and an index on email"
}]
}
}
# Plan: migrate-schema (no provision)
# Q&A:
{
"id": "t2",
"message": {
"metadata": { "database_id": "<id>" },
"parts": [{"text":
"top 5 customers by total order value"
}]
}
}
# Plan: chat-with-data → SELECT → answer# Migrate:
{
"jsonrpc": "2.0", "id": 1,
"method": "tasks/send",
"params": {
"id": "t1",
"message": {
"metadata": { "database_id": "<id>" },
"parts": [{"text":
"add a deleted_at column to users"
}]
}
}
}
# Q&A:
{
"jsonrpc": "2.0", "id": 2,
"method": "tasks/send",
"params": {
"id": "t2",
"message": {
"metadata": { "database_id": "<id>" },
"parts": [{"text":
"top 5 customers by order value"
}]
}
}
}Set metadata.skill to bypass the planner entirely. Free-form text in parts is ignored when skill is explicit. Use when you want a single skill invoked exactly as specified.
# Run a specific SQL — no LLM in the loop:
{
"id": "t3",
"message": {
"metadata": {
"skill": "exec-sql",
"database_id": "<id>",
"sql": "SELECT count(*) FROM users
WHERE deleted_at IS NULL"
}
}
}{
"jsonrpc": "2.0", "id": 1,
"method": "tasks/send",
"params": {
"id": "t3",
"message": {
"metadata": {
"skill": "exec-sql",
"database_id": "<id>",
"sql": "SELECT count(*) FROM users
WHERE deleted_at IS NULL"
}
}
}
}# Available metadata.skill values: provision-postgres, get-status, exec-sql, list-my-dbs, extend-ttl, delete-db, create-schema, migrate-schema, load-sample-data, add-procedure, describe-schema, generate-orm-models, generate-query, chat-with-data, reset-schema, export-data
Provision → design schema → generate types. Explicit skill control. Each call is a separate submit; capture database_id from step 1's task result.
# 1. provision
{ "id":"s1", "message": { "metadata": {
"skill": "provision-postgres",
"ttl_minutes": 5
} } }
# 2. create schema
{ "id":"s2", "message": { "metadata": {
"skill": "create-schema",
"database_id": "<id from step 1>",
"description": "e-commerce: users,
products, categories, orders,
order_items"
} } }
# 3. generate TypeScript interfaces
{ "id":"s3", "message": { "metadata": {
"skill": "generate-orm-models",
"database_id": "<id>",
"language": "typescript"
} } }# 1. provision
{ "jsonrpc":"2.0", "id":1, "method":"tasks/send",
"params": { "id":"s1", "message": { "metadata": {
"skill": "provision-postgres",
"ttl_minutes": 5
} } } }
# 2. create schema
{ "jsonrpc":"2.0", "id":2, "method":"tasks/send",
"params": { "id":"s2", "message": { "metadata": {
"skill": "create-schema",
"database_id": "<id from step 1>",
"description": "e-commerce schema..."
} } } }
# 3. generate TypeScript interfaces
{ "jsonrpc":"2.0", "id":3, "method":"tasks/send",
"params": { "id":"s3", "message": { "metadata": {
"skill": "generate-orm-models",
"database_id": "<id>",
"language": "typescript"
} } } }Ask questions in plain English; get rows + a written answer.
# Via workflow:
{ "id":"q1", "message": { "parts":[{"text":
"create an orders DB, seed 20 rows,
what is the average order value?"
}]}}
# Or direct skill on existing DB:
{ "id":"q2", "message": { "metadata": {
"skill": "chat-with-data",
"database_id": "<id>",
"question": "average order value
per customer last month?"
} } }# Via workflow:
{ "jsonrpc":"2.0", "id":1, "method":"tasks/send",
"params": { "id":"q1", "message": { "parts":[{"text":
"create an orders DB, seed 20 rows,
what is the average order value?"
}]}}}
# Or direct skill on existing DB:
{ "jsonrpc":"2.0", "id":2, "method":"tasks/send",
"params": { "id":"q2", "message": { "metadata": {
"skill": "chat-with-data",
"database_id": "<id>",
"question": "average order value
per customer last month?"
} } } }Result (either shape): { "sql": "...", "rows": [...], "answer": "Average order value was $87.40..." }
Apply incremental migrations without dropping tables.
{ "id":"m1", "message": { "metadata": {
"skill": "migrate-schema",
"database_id": "<id>",
"description":
"add a status column to orders with
values pending, paid, shipped"
} } }{ "jsonrpc":"2.0", "id":1, "method":"tasks/send",
"params": { "id":"m1", "message": { "metadata": {
"skill": "migrate-schema",
"database_id": "<id>",
"description":
"add a status column to orders with
values pending, paid, shipped"
} } } }Ownership tokens (recommended)
Pass a 32+ char random token at submit time via the Authorization: Bearer header (works on both wire formats — header is below the body shape). Your task and any provisioned DBs are bound to that token — only callers presenting it can poll the task or use the DB.
TOKEN=$(openssl rand -hex 24)
curl -X POST https://api.dbaas.dev/a2a/tasks/send \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"id":"x","message":{"parts":[
{"text":"create a blog DB and seed it"}
]}}'
# poll
curl https://api.dbaas.dev/a2a/tasks/$TASK \
-H "Authorization: Bearer $TOKEN"TOKEN=$(openssl rand -hex 24)
curl -X POST https://api.dbaas.dev/a2a \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tasks/send",
"params":{"id":"x","message":{"parts":[
{"text":"create a blog DB and seed it"}
]}}}'
# poll
curl -X POST https://api.dbaas.dev/a2a \
-H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":2,"method":"tasks/get",
"params":{"id":"<task-uuid>"}}'Server stores SHA-256 hash only. Constant-time compare on every check.
First poll returns connection_string. Subsequent polls return [redacted] — capture on first read.
Authentication
Card advertises authentication.schemes: ["Bearer"] with required: false. Three places the token can live (in priority order):
Authorization: Bearer <token>Standard A2A spec auth. Recommended.
X-Client-Token: <token>Useful when SDK can't set Authorization.
metadata.client_tokenInside message.metadata (flat) or params.message.metadata (RPC).
Token format: 32–128 char opaque random string. Server stores SHA-256 hash; constant-time compare on every poll. Operators can flip A2A_REQUIRE_CLIENT_TOKEN=true on the backend to make submission without a token return 401; the card's authentication.required field will reflect this.
Error response shapes
Both wire formats use the same numeric error codes (per JSON-RPC 2.0). Only the envelope differs.
# Unknown method:
HTTP/1.1 400 Bad Request
{
"jsonrpc": "2.0",
"id": 99,
"error": {
"code": -32601,
"message": "Method not found: frobnicate"
}
}
# Task not found (auth-fail looks the
# same — deliberate, hides existence):
HTTP/1.1 404 Not Found
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32001,
"message": "Task not found" }
}# Skill not determined:
HTTP/1.1 400 Bad Request
{
"error": {
"code": -32602,
"message": "Could not determine skill..."
}
}
# Task not found:
HTTP/1.1 404 Not Found
{
"error": { "code": -32001,
"message": "Task not found" }
}The id field round-trips verbatim in JSON-RPC errors (string · number · null preserved). HTTP status code mirrors the JSON-RPC error: 400 for client errors, 401 for missing-required-token, 404 for not-found, 413 for oversized input, 500 for internal, 501 for not-implemented methods (cancel, sendSubscribe), 429 for rate-limit.
Limits
Mirrored verbatim by the agent card's limits object — clients can pre-flight without trial-and-error.
- No API key required — public agent. Rate limit 5 submits / 10 min / IP (
limits.submitsPerTenMinutes: 5). Returns429when exceeded. - Polling rate limit 120 req / min / IP (
limits.pollsPerMinute: 120). - Per-IP cap of 2 concurrent active ephemeral databases (
limits.concurrentDatabasesPerIP: 2). - Database TTL 1–60 min (default 60). Extend up to 90 min total via
extend-ttl(limits.databaseTTLMinutes: {min:1, max:60, default:60, extendCapMinutes:90}). - Input length caps: 4 KB per free-form text field (
limits.maxTextLengthBytes: 4096), 32 KB for raw SQL (limits.maxSQLLengthBytes: 32768). Returns413when exceeded. - LLM-generated SQL filtered (rejects COPY, pg_read_file, ALTER SYSTEM, shell metacharacters).
Agent Card
Machine-readable A2A v0.2 card — every spec-required field plus dbaas extensions. Cached public, max-age=3600. Both /.well-known/agent.json (legacy) and /.well-known/agent-card.json (A2A v0.2 canonical) return identical content — pick whichever your spec client fetches.
{
// ─── A2A v0.2 spec-required ──────────────────────────────────────────
"name": "dbaas.dev",
"description": "PostgreSQL agent that plans and executes ...",
"url": "https://api.dbaas.dev/a2a",
"version": "2.4.0",
"documentationUrl": "https://dbaas.dev/integrations/a2a",
"provider": { "organization": "dbaas.dev", "url": "https://dbaas.dev" },
"capabilities": {
"streaming": false, // tasks/sendSubscribe (SSE) not implemented
"pushNotifications": false, // no webhook delivery yet
"stateTransitionHistory": true, // submitted → working → completed/failed persisted
"async": true, // (extension)
"taskPolling": true // (extension)
},
"authentication": {
"schemes": ["Bearer"],
"required": false, // optional unless A2A_REQUIRE_CLIENT_TOKEN=true
"description": "Optional Authorization: Bearer <32-128 char client_token>..."
},
"defaultInputModes": ["application/json", "text/plain"],
"defaultOutputModes": ["application/json"],
// ─── A2A spec extensions (dbaas-specific, namespaced) ───────────────
"protocol": "a2a",
"protocolVersion": "0.2",
"defaultSkill": "workflow",
"primaryInterface": "natural-language",
"interfaces": [
{
"type": "json-rpc",
"url": "https://api.dbaas.dev/a2a",
"contentType": "application/json",
"methods": ["tasks/send", "tasks/get"],
"unsupportedMethods": ["tasks/cancel", "tasks/sendSubscribe"],
"errorCodes": {
"-32700": "Parse error",
"-32600": "Invalid Request",
"-32601": "Method not found",
"-32602": "Invalid params",
"-32603": "Internal error",
"-32001": "Task not found"
}
},
{
"type": "rest-legacy",
"url": "https://api.dbaas.dev/a2a/tasks/send",
"contentType": "application/json",
"body": "flat: {id, message{role, parts[], metadata}}",
"poll": "GET /a2a/tasks/{task_id}"
}
],
"endpoints": {
"submit": "POST /a2a/tasks/send", // legacy flat
"poll": "GET /a2a/tasks/{task_id}", // legacy poll
"jsonrpc": "POST /a2a", // canonical A2A v0.2
"jsonrpcGet": "POST /a2a method=\"tasks/get\""
},
"limits": {
"submitsPerTenMinutes": 5,
"pollsPerMinute": 120,
"maxTextLengthBytes": 4096,
"maxSQLLengthBytes": 32768,
"databaseTTLMinutes": { "min": 1, "max": 60, "default": 60, "extendCapMinutes": 90 },
"concurrentDatabasesPerIP": 2
},
"skills": [ /* workflow + 16 direct-access, each with id/name/description/tags/examples/inputSchema */ ]
}