Skip to content
AbloAblo
Esc
navigateopen⌘Jpreview
On this page

Agents

The stateless participant: wake on a trigger, read, claim, commit, go idle.

An agent is a reactive participant: it wakes on something happening, reads what it needs, writes a result, and goes idle. That’s a request/response workload — so agents talk to Ablo over plain HTTP, holding no WebSocket. The credential is the identity; the server resolves the org, scope, and actor from the key on every request.

Agents get the stateless plane (HTTP). People — when you add the humans() plugin — get the live plane (WebSocket: presence, optimistic, sub-100ms). Both operate on the same typed, coordinated state — and coordinate with each other.

The agent client

Same Ablo() entry point as everywhere else — pass transport: 'http'. No socket, no connection state — just your schema (for types) and an API key.

import Ablo from "@abloatai/ablo";
import { schema } from "./schema";

const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: "http" });

// Reads + writes, fully typed off your schema.
// `get` resolves to the row, or `undefined` when none matches.
const open = await ablo.tasks.list({ where: { status: "todo" } });

const task = await ablo.tasks.get({ id: open[0].id });
if (!task) throw new Error("task not found");

console.log(task.title);
await ablo.tasks.update({ id: task.id, data: { status: "done" } });

It exposes get / list / create / update / delete, plus commits and claim. It does not expose stateful-only local reads or onChange subscriptions. Those need a live connection, so with transport: 'http' they are compile errors rather than runtime surprises.

AI SDK tools

Keep AI SDK in charge of the model loop and expose only the Ablo operations the model needs:

import { generateText } from 'ai';
import {
  createTool,
  deleteTool,
  readTool,
  updateTool,
} from '@abloatai/ablo/ai-sdk';

const tools = {
  getTask: readTool(ablo.tasks, {
    description: 'Read the current task.',
    inputSchema: z.object({ taskId: z.string() }),
    id: ({ taskId }) => taskId,
  }),
  createTask: createTool(ablo.tasks, {
    description: 'Create a task.',
    inputSchema: z.object({ requestId: z.string(), title: z.string() }),
    id: ({ requestId }) => requestId,
    data: ({ title }) => ({ title, status: 'todo' }),
  }),
  updateTask: updateTool(ablo.tasks, {
    description: 'Update a task without overwriting concurrent work.',
    inputSchema: z.object({ taskId: z.string(), status: z.string() }),
    id: ({ taskId }) => taskId,
    apply: (_current, { status }) => ({ status }),
  }),
  deleteTask: deleteTool(ablo.tasks, {
    description: 'Delete a task after taking its claim.',
    inputSchema: z.object({ taskId: z.string() }),
    id: ({ taskId }) => taskId,
    // Destructive tools require AI SDK approval by default.
  }),
};

await generateText({ model, messages, tools });

These are adapters over the same typed resources used by ordinary backend code. Ablo does not own the planner, prompt system, memory, provider, worker, or workflow runtime.

Coordination: claim, queue, reorder

The differentiator. A claim is a durable lease + FIFO wait-line on a row — “who’s working on this, who’s waiting” — and it’s request/response, so an agent holds it over HTTP. This is how two agents (or an agent and a human) don’t clobber the same record.

// Acquire a lease, do work with the held row, release on scope exit:
await using claim = await ablo.tasks.claim({ id: taskId });
const task = claim.data;
// …no one else can hold this row while you work…
await ablo.tasks.update({
  id: task.id,
  data: { status: "in_review" },
  claim,
});

await ablo.tasks.claim.state({ id: taskId });   // who holds it now (or null)
await ablo.tasks.claim.queue({ id: taskId });   // the FIFO wait-line behind the holder
await ablo.tasks.claim.reorder({ id: taskId, order: line }); // re-rank the line (privileged)

Think of it as a queue per row — a durable, inspectable, reorderable lease line (“SQS for entity contention”). Use { queue: false } for fail-fast dedup: if someone else has this job, skip it.

Messaging between agents

Use claim description and meta for live “what I am doing now” context. Use ordinary synced rows for handoffs, status notes, and requests that must survive reconnects or be readable by HTTP agents. The recipe is a messages model scoped by the same syncGroup field as the work row, with aboutIntentId linking a message back to the claim it discusses.

See Agent Messaging for the schema and setup details.

When a person is in the loop

There’s no separate “agent mode” — and no separate human mode either. The bare client is the coordination layer; humans() is the plugin that adds the live plane on top of it. An agent acting over HTTP and a person editing over their socket share the same typed state and the same coordination: the agent can claim the row that person is holding (and wait in line), and they see the agent’s committed changes stream in live over their own socket, even though the agent committed over HTTP.

There is no agents() plugin, and the absence is the point — an agent is the default caller here, not a bolt-on.

How an agent runs

something happens ──▶ your agent (HTTP, no socket)
  (a job, a webhook,     read context (list/retrieve)
   a queue message)      claim → work → commit
                         done — no held connection

Because it holds nothing open, an agent is a stateless worker: deploys and restarts are free, and you scale by adding workers. A long-running fleet of idle agents costs nothing on the live plane — that capacity stays for humans.

What stays on the live (human) plane

onChange (live subscriptions) and the local reads (local synced-pool reads) require a WebSocket and a local store — they’re for interactive UIs, not stateless agents. An agent reacts to an external trigger (a job/queue/webhook), then reads with list/get. See client behavior for the full surface and guarantees for the coordination semantics.

Was this page helpful?