Imported from Abloatai/ablo (
packages/ablo/AGENTS.md). Install upstream withnpx skills add Abloatai/ablo --skill ablo. Copyright stays with the author.
AGENTS.md
Ablo lets AI agents and humans safely edit the same typed data without clobbering each other. When two of them touch the same row, a "claim" makes one wait for the other instead of overwriting it. For an existing application, first preserve its named operation and persistence boundary; use the schema-backed row pattern below only when that route owns the write.
Claims don't lock. If another writer holds the row, claim waits for them and re-reads the fresh row before handing it to you — so two writers serialize instead of clobbering.
Groups structure which shared state reaches each person or agent. Declare the data's scope and participant authority, then let authorized subscriptions keep reactive views current; HTTP agents explicitly read or observe changes. Start with Groups and shared context to connect membership, access, loading and updates.
Start here — scaffold with ablo init
Before choosing among identifier claims, row claims, captured reads, atomic commits, existing database writes, and Ablo-routed writes, use the coordinate existing work guide. It routes existing applications to the smallest relevant example and names the test layer that proves each guarantee.
Don't hand-write the integration. Run the CLI; it generates the current-API schema, client, the database connection (logical replication by default, or a signed Data Source endpoint as the fallback), and (for Next.js) the browser provider + session route:
- Read the docs for THIS version:
npx ablo docslists every page,npx ablo docs <page>prints one. They ship inside the installed package, so they describe the code innode_modulesand work with no network. Read them instead of a docs URL — a website describes the newest release, so against a pinned version it will hand you a call your package doesn't have (retrieve/listreplacedget/getAll/getCountin 0.35.0). - Scaffold:
npx ablo init --yes— flag-driven, never prompts. Override defaults with--framework <nextjs|vite|remix|vanilla>,--auth <apikey|…>,--no-agent,--no-pull,--no-install,--no-login. (Plainablo initneeds a TTY and will HANG in an agent/CI run — always pass--yes.) - Auth: set
ABLO_API_KEYin the environment. Do NOT runablo login— it opens a browser device flow and blocks an agent. - Connect your database — logical replication (the primary path):
npx ablo connect apply --url <postgres-url>provisions scoped roles and replication using the admin credential supplied for that command. Model writes then go through Ablo and land in your Postgres; its change stream confirms them. Ablo does not own your rows or run application-schema migrations. Your ORM remains responsible for tables, columns, and constraints. - Fallback — signed Data Source endpoint (DB can't grant a
REPLICATIONrole): the generatedablo/data-source.tsexposes one route; Ablo sends signed requests and your app touches its own DB. Only in this mode doesnpx ablo migrateprovision the adapter's bookkeeping tables (ablo_outbox,ablo_idempotency) plus your Ablo models — it does not touch your other tables. Keep your own migrations (drizzle-kit / prisma migrate) for auth and anything outside the Ablo schema. - No database yet? Run
npx ablo dev --no-watch --branch <name>to create an isolated non-root branch and obtain its expiringsk_credential. The branch uses a throwaway hosted data plane; Production remains the protected root. There is no shared Sandbox mode. - Adopt an existing DB schema:
npx ablo pull prisma [path]/pull drizzle <module>(lossless) orpull(live DB, lossy). Writesablo/schema.ts. - Prepare a branch and push your schema — REQUIRED before any write works. The server keeps its OWN copy of the schema. After you create or edit
ablo/schema.ts, runnpx ablo dev --no-watch --branch <git-or-ci-ref>: it ensures an isolated branch, mints a temporary branch credential, writes it to gitignored.env.local, pushes once, and exits. Skip this and every write to a new or changed model fails withserver_execute_unknown_model. Plainablo devwatches forever — never run it bare in an agent.ablo pushis the lower-level one-shot command for an already selected key, including reviewed production deploys. - Do not invent one key variable per environment. Branches replaced manual
ABLO_STAGING_KEY/ABLO_DEV_KEYswitching. Runtime code always readsABLO_API_KEY;ablo devwires the Git branch's expiring key. Usenpx ablo whoamifor the strict server-confirmed project/branch, ornpx ablo whoami --key-env <NAME>only to inspect an explicitly named CI/legacy/recovery key without copying its value into argv. - Other long-running:
npx ablo logs --no-follow(default tails forever).status,branch check,push,pull,check,generate,docs, anddev --no-watchare one-shot.
When you use the signed-endpoint fallback, the generated ablo/data-source.ts is the whole endpoint and needs no hand-editing: dataSourceNext({ schema, apiKey, adapter: prismaDataSource(prisma, schema) }) (or drizzleDataSource(db, schema)). The adapter owns commit / idempotency / outbox.
Working on a real database? Plain model writes are last-write-wins when no active claim applies. Use a functional update, a held claim, or read plus reads when a result depends on an earlier value. Reads are safe to inspect; raw application DDL (ALTER TABLE …) and a --yes connection cutover belong to a human. When you're unsure whether a write fits, npx ablo check reports the live column-by-column fit read-only, before anything runs. Full sorting rule: Operating on Your Database.
Rule
Edit the generated files; teach this API only:
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
The schema is the integration contract — it drives typed model clients, React selectors, server and agent writes, the Data Source shape, and schema push. Ablo owns only the models you declare; your auth and other non-synced tables stay in your own ORM schema, side by side in the same database. Don't create a parallel string-keyed write path for rows that belong to a schema model.
Every model verb takes ONE options object. The common loop:
- Get or read the row —
get({ id })observes;read({ id })declares that a later mutation depends on this exact version.list({ where })is observational. In React render, uselocal.get(id). - See who's active (optional) —
ablo.<model>.claim.state({ id })(synchronous; never blocks). - Claim the row before changing it —
await using claim = await ablo.<model>.claim({ id, description?, ttl? }). If someone else holds it, this waits for them, then gives you the fresh row onclaim.data. The claim auto-releases when it goes out of scope (await using). - Write — pass
reads: [row]when the decision used a row returned byread, or passclaimwhen writing through a held claim. If the declared read moved or the claim was lost, the mutation does not land.
Keep coding assistants on this schema-backed path.
Minimal example
import Ablo from '@abloatai/ablo';
import { defineSchema, model, z } from '@abloatai/ablo/schema';
const schema = defineSchema({
weatherReports: model({
location: z.string(),
status: z.enum(['pending', 'ready']),
forecast: z.string().optional(),
}),
});
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
const report = await ablo.weatherReports.read({ id: 'report_stockholm' });
if (!report) throw new Error('Report not found');
// If someone else holds the row, claim waits for them and re-reads the fresh
// row before resolving. Auto-released at the end of this scope (`await using`).
await using claim = await ablo.weatherReports.claim({
id: 'report_stockholm',
description: 'forecasting',
ttl: '2m',
});
const claimed = claim.data;
// Because we hold the claim, update is rejected if the row changed underneath us.
await ablo.weatherReports.update({
id: claimed.id,
data: { status: 'ready', forecast: await getForecast(claimed.location) },
claim,
});
Coordination surface
Claims live on a callable namespace beside create / update / get. Every member takes an options object:
await using claim = await ablo.<model>.claim({ id })— acquire the row (waits if held); read it viaclaim.data; auto-releases on scope exit (or callclaim.release()).ablo.<model>.claim.state({ id })— who is currently working on the row (synchronous; never blocks).ablo.<model>.claim.queue({ id })— who is waiting behind the current holder.ablo.<model>.claim.release({ id })— release a claim early.ablo.<model>.claim.reorder({ id, order })— reorder the waiting queue.
Keep admission behavior together for anything beyond the default wait:
const claim = await ablo.records.claim({
id,
contention: {
mode: 'skip', // use 'wait' with maxDepth / timeoutMs when waiting is useful
onStatus(event) {
if (event.type === 'skipped') console.warn(event.error.message);
},
},
});
if (!claim) return; // another participant already owns the work
onStatus receives typed queued, granted, skipped, and failed events.
It is request-scoped; use claim.state / claim.queue for the shared reactive
view.
Most users declare a schema and write through ablo.<model>.update({ id, data }).