@abloatai/ablo 0.51.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -90,7 +90,7 @@ Claims live on a callable namespace beside `create` / `update` / `get`. Every me
90
90
  Keep admission behavior together for anything beyond the default wait:
91
91
 
92
92
  ```ts
93
- const claim = await ablo.tasks.claim({
93
+ const claim = await ablo.records.claim({
94
94
  id,
95
95
  contention: {
96
96
  mode: 'skip', // use 'wait' with maxDepth / timeoutMs when waiting is useful
package/CHANGELOG.md CHANGED
@@ -1,23 +1,93 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.52.0
4
+
5
+ ### Models carry only `id`
6
+
7
+ `createdAt`, `updatedAt`, `organizationId`, and `createdBy` are no longer added
8
+ to every model. Declare them as ordinary fields wherever you want them, and
9
+ declare them to keep reading and writing them if you relied on Ablo supplying
10
+ them. Ablo still records who made each change in its own transaction log, and
11
+ still owns the tenancy value on every write.
12
+
13
+ A model can point at a table Ablo did not create, naming the columns that
14
+ differ:
15
+
16
+ ```ts
17
+ import { defineSchema, field, model } from '@abloatai/ablo/schema';
18
+
19
+ export const schema = defineSchema({
20
+ itemEvents: model(
21
+ {
22
+ itemId: field.string().from('item_id'),
23
+ createdAt: field.number().from('created_at'),
24
+ },
25
+ { tableName: 'item_events' }
26
+ ),
27
+ });
28
+ ```
29
+
30
+ Database adapters accept identifiers the database generates and return them as
31
+ canonical string ids, taking the id type from the connection rather than from
32
+ the model.
33
+
34
+ ### Updates can carry a precondition
35
+
36
+ An update operation accepts `where`. The database changes the row only while its
37
+ current values still match. On a mismatch the commit fails with
38
+ `precondition_failed` and the whole batch declines, leaving every operation in it
39
+ unapplied. The Kysely source adapter supports preconditions; the Drizzle,
40
+ Prisma, and memory adapters report `source_adapter_misconfigured`.
41
+
42
+ ### Commit receipts return the rows the database wrote
43
+
44
+ Receipts carry `operationResults`, pairing each operation's `transactionId` with
45
+ its outcome and the authoritative row the database transaction returned,
46
+ including identifiers and timestamps the database generated.
47
+
48
+ ### Two error codes renamed
49
+
50
+ `task_id_missing` is now `item_id_missing`, and `task_id_required` is now
51
+ `item_id_required`. Neither old code was ever returned by a request, so a caller
52
+ matching on error codes has nothing to change unless it names one directly.
53
+
54
+ ### CLI
55
+
56
+ `ablo setup` reads the repository and the current Ablo target, then prints the
57
+ decisions, actions, blockers, and postconditions a verified setup requires. It
58
+ reports and leaves the project untouched.
59
+
60
+ `ablo init --plan` shows every file action before any of it happens.
61
+
62
+ `ablo telemetry` controls limited CLI usage analytics. Collection is on by
63
+ default and stays off in continuous integration and whenever `DO_NOT_TRACK=1` or
64
+ `ABLO_TELEMETRY_DISABLED=1` is set. Run `ablo telemetry status` to see the
65
+ current state, `ablo telemetry disable` to turn collection off, and
66
+ `ablo telemetry reset` to rotate the local installation identity.
67
+
3
68
  ## 0.51.0
4
69
 
5
- ### Minor Changes
70
+ ### One platform schema can serve every customer organization
6
71
 
7
- - 3a25ab4: Default cross-organization user sessions to the platform key's schema project.
8
- Customer data remains isolated in the target organization, while one pushed
9
- schema can describe every customer tenant. `sessions.create` also accepts an
10
- explicit `schemaProject` override for migrations and advanced routing.
72
+ Platforms no longer need to copy the same schema into every customer
73
+ organization. When a platform key creates a session for another organization,
74
+ Ablo now reads the schema from the platform's project while keeping every row
75
+ inside the customer's organization.
11
76
 
12
- The sessions guide now distinguishes policy-scoped customers from structurally
13
- isolated customer organizations and makes clear that sync-group routing is not
14
- read authorization.
77
+ ```ts
78
+ const { token } = await ablo.sessions.create({
79
+ user: { id: userId },
80
+ organizationId: customerOrganizationId,
81
+ can: { records: ['read', 'update'] },
82
+ });
83
+ ```
15
84
 
16
- ### Patch Changes
85
+ Most platforms need no schema option at all. Migrations and advanced routing can
86
+ still select one explicitly with `schemaProject`.
17
87
 
18
- - Updated dependencies [3a25ab4]
19
- - @abloatai/transaction@0.51.0
20
- - @abloatai/humans@0.51.0
88
+ The sessions guide now draws a firm line between policy-scoped customers and
89
+ separate customer organizations. Sync groups decide which changes travel; they
90
+ do not authorize reads.
21
91
 
22
92
  ## 0.50.0
23
93
 
@@ -31,14 +101,14 @@ and carries exact Ablo rows into `ctx.reads` for the write that follows.
31
101
  const ctx = await context({
32
102
  ablo,
33
103
  data: {
34
- task: ablo.tasks.get({ id: taskId }),
35
- documents: ablo.documents.list({ where: { taskId } }),
36
- memory: loadMemories(taskId),
104
+ record: ablo.records.get({ id: recordId }),
105
+ records: ablo.records.list({ where: { recordId } }),
106
+ memory: loadMemories(recordId),
37
107
  },
38
108
  });
39
109
 
40
- await ablo.tasks.update({
41
- id: taskId,
110
+ await ablo.records.update({
111
+ id: recordId,
42
112
  data: result,
43
113
  reads: ctx.reads,
44
114
  });
@@ -64,11 +134,11 @@ write lands anyway, on top of a decision that is no longer true. Pass the rows
64
134
  the decision was based on:
65
135
 
66
136
  ```ts
67
- const task = await ablo.tasks.get({ id: taskId });
68
- await ablo.tasks.update({
69
- id: task.id,
70
- data: { status: 'done', result: `Completed: ${task.title}` },
71
- reads: [task],
137
+ const record = await ablo.records.get({ id: recordId });
138
+ await ablo.records.update({
139
+ id: record.id,
140
+ data: { status: 'done', result: `Completed: ${record.title}` },
141
+ reads: [record],
72
142
  });
73
143
  ```
74
144
 
@@ -89,7 +159,7 @@ turn or skips, expires on its own, and keeps itself alive with a heartbeat while
89
159
  the agent works.
90
160
 
91
161
  If an agent loses its claim during a model call, its final write is refused. Two
92
- agents cannot both believe they own the same task and both write, and a slow
162
+ agents cannot both believe they own the same record and both write, and a slow
93
163
  agent cannot land its answer on top of whoever picked the work up after it. Ablo
94
164
  decides who holds the claim, so an agent cannot assert one it does not have.
95
165
 
@@ -114,7 +184,7 @@ database you connected.
114
184
 
115
185
  An agent holding a key can now ask what that key permits and get the answer from
116
186
  Ablo, whether it keeps a connection open or calls over HTTP for a single turn.
117
- An agent that mints a narrower key for a sub-task can confirm what it handed
187
+ An agent that mints a narrower key for a sub-record can confirm what it handed
118
188
  over.
119
189
 
120
190
  ### A refused action says which permission was missing
@@ -307,7 +377,7 @@ New clients can pin their intended project and branch with `projectId` /
307
377
  `ABLO_PROJECT_ID` and `branchId` / `ABLO_BRANCH_ID`. `ablo dev` writes both
308
378
  immutable coordinates beside its branch-bound key, and `ready()` compares them
309
379
  with the key's server-resolved target before opening the sync connection. A mail
310
- deployment carrying a slides key now fails with `project_scope_denied`; a
380
+ deployment carrying an unrelated key now fails with `project_scope_denied`; a
311
381
  same-project key for the wrong environment fails with `branch_scope_denied`.
312
382
 
313
383
  ### Pre-existing rows arrive on their own
@@ -350,7 +420,7 @@ end-to-end regression pins the behavior.
350
420
  The new `contention` option names what happens when the row is already held:
351
421
 
352
422
  ```ts
353
- const claim = await ablo.tasks.claim({
423
+ const claim = await ablo.records.claim({
354
424
  id,
355
425
  contention: {
356
426
  mode: 'skip',
package/NOTICE CHANGED
@@ -2,7 +2,7 @@
2
2
  Copyright 2025-2026 Lukas Andersson
3
3
 
4
4
  This product includes software developed by Lukas Andersson
5
- (https://ablo.finance).
5
+ (https://abloatai.com).
6
6
 
7
7
  "Ablo" is a trademark of Lukas Andersson. This license does not grant
8
8
  permission to use the Ablo name, logo, or trademarks. Third parties
package/docs/agents.md CHANGED
@@ -14,8 +14,8 @@ plugin — get the live plane (WebSocket: presence, optimistic, sub-100ms).
14
14
  other*.**
15
15
 
16
16
  <Note>
17
- Agents transact against your **pushed schema**, same as everyone — `ablo.tasks`
18
- exists because you defined a `task` model and ran `ablo push`. The key
17
+ Agents transact against your **pushed schema**, same as everyone — `ablo.records`
18
+ exists because you defined a `record` model and ran `ablo push`. The key
19
19
  authenticates; the [schema](/quickstart) defines what you can call.
20
20
  </Note>
21
21
 
@@ -32,13 +32,13 @@ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: "http"
32
32
 
33
33
  // Reads + writes, fully typed off your schema.
34
34
  // `get` resolves to the row, or `undefined` when none matches.
35
- const open = await ablo.tasks.list({ where: { status: "todo" } });
35
+ const open = await ablo.records.list({ where: { status: "todo" } });
36
36
 
37
- const task = await ablo.tasks.get({ id: open[0].id });
38
- if (!task) throw new Error("task not found");
37
+ const record = await ablo.records.get({ id: open[0].id });
38
+ if (!record) throw new Error("record not found");
39
39
 
40
- console.log(task.title);
41
- await ablo.tasks.update({ id: task.id, data: { status: "done" } });
40
+ console.log(record.title);
41
+ await ablo.records.update({ id: record.id, data: { status: "done" } });
42
42
  ```
43
43
 
44
44
  It exposes `get` / `list` / `create` / `update` / `delete`, plus `commits`
@@ -61,27 +61,27 @@ import {
61
61
  } from '@abloatai/ablo/ai-sdk';
62
62
 
63
63
  const tools = {
64
- getTask: readTool(ablo.tasks, {
65
- description: 'Read the current task.',
66
- inputSchema: z.object({ taskId: z.string() }),
67
- id: ({ taskId }) => taskId,
64
+ getTask: readTool(ablo.records, {
65
+ description: 'Read the current record.',
66
+ inputSchema: z.object({ recordId: z.string() }),
67
+ id: ({ recordId }) => recordId,
68
68
  }),
69
- createTask: createTool(ablo.tasks, {
70
- description: 'Create a task.',
69
+ createTask: createTool(ablo.records, {
70
+ description: 'Create a record.',
71
71
  inputSchema: z.object({ requestId: z.string(), title: z.string() }),
72
72
  id: ({ requestId }) => requestId,
73
73
  data: ({ title }) => ({ title, status: 'todo' }),
74
74
  }),
75
- updateTask: updateTool(ablo.tasks, {
76
- description: 'Update a task without overwriting concurrent work.',
77
- inputSchema: z.object({ taskId: z.string(), status: z.string() }),
78
- id: ({ taskId }) => taskId,
75
+ updateTask: updateTool(ablo.records, {
76
+ description: 'Update a record without overwriting concurrent work.',
77
+ inputSchema: z.object({ recordId: z.string(), status: z.string() }),
78
+ id: ({ recordId }) => recordId,
79
79
  apply: (_current, { status }) => ({ status }),
80
80
  }),
81
- deleteTask: deleteTool(ablo.tasks, {
82
- description: 'Delete a task after taking its claim.',
83
- inputSchema: z.object({ taskId: z.string() }),
84
- id: ({ taskId }) => taskId,
81
+ deleteTask: deleteTool(ablo.records, {
82
+ description: 'Delete a record after taking its claim.',
83
+ inputSchema: z.object({ recordId: z.string() }),
84
+ id: ({ recordId }) => recordId,
85
85
  // Destructive tools require AI SDK approval by default.
86
86
  }),
87
87
  };
@@ -102,18 +102,18 @@ clobber the same record.
102
102
 
103
103
  ```ts
104
104
  // Acquire a lease, do work with the held row, release on scope exit:
105
- await using claim = await ablo.tasks.claim({ id: taskId });
106
- const task = claim.data;
105
+ await using claim = await ablo.records.claim({ id: recordId });
106
+ const record = claim.data;
107
107
  // …no one else can hold this row while you work…
108
- await ablo.tasks.update({
109
- id: task.id,
108
+ await ablo.records.update({
109
+ id: record.id,
110
110
  data: { status: "in_review" },
111
111
  claim,
112
112
  });
113
113
 
114
- await ablo.tasks.claim.state({ id: taskId }); // who holds it now (or null)
115
- await ablo.tasks.claim.queue({ id: taskId }); // the FIFO wait-line behind the holder
116
- await ablo.tasks.claim.reorder({ id: taskId, order: line }); // re-rank the line (privileged)
114
+ await ablo.records.claim.state({ id: recordId }); // who holds it now (or null)
115
+ await ablo.records.claim.queue({ id: recordId }); // the FIFO wait-line behind the holder
116
+ await ablo.records.claim.reorder({ id: recordId, order: line }); // re-rank the line (privileged)
117
117
  ```
118
118
 
119
119
  Think of it as a queue per row — a durable, inspectable, reorderable lease
package/docs/api-keys.md CHANGED
@@ -119,9 +119,9 @@ knobs, and you set exactly one.
119
119
 
120
120
  | Mint | Call | Result |
121
121
  |---|---|---|
122
- | Human end-user session | `await server.sessions.create({ user: { id }, can: { tasks: ['read'] } })` | `ek_` (scoped to `can`) |
123
- | Ready agent client | `await server.agents.create({ can: { tasks: ['update'] } })` | Auto-refreshing client scoped to `can` |
124
- | Raw delegated agent token | `await server.sessions.create({ agent: { id }, can: { tasks: ['update'] } })` | `rk_` for another runtime |
122
+ | Human end-user session | `await server.sessions.create({ user: { id }, can: { records: ['read'] } })` | `ek_` (scoped to `can`) |
123
+ | Ready agent client | `await server.agents.create({ can: { records: ['update'] } })` | Auto-refreshing client scoped to `can` |
124
+ | Raw delegated agent token | `await server.sessions.create({ agent: { id }, can: { records: ['update'] } })` | `rk_` for another runtime |
125
125
 
126
126
  The principal kind comes from *which* shape you pass — `{ user, can }` → `user`, `{ agent, can }` → `agent`.
127
127
 
@@ -217,6 +217,12 @@ restricted to exactly those grants:
217
217
  - `project:manage` — list, create, and rename projects.
218
218
  - `branch:manage` — list, create, and delete child branches and mint their
219
219
  temporary credentials.
220
+ - `ephemeral:mint-any-org` — cross-organization authority to mint a short-lived
221
+ user session into a customer organization. It follows the Stripe Connect shape:
222
+ the request names the customer organization, but the resulting session is
223
+ still bounded by its `can` grant and expiry. A key restricted to this scope
224
+ cannot directly read or write customer organizations' rows, push schema, or
225
+ manage projects.
220
226
 
221
227
  Both management scopes are explicit grants on `mk_` credentials. Runtime
222
228
  `sk_`, `rk_`, `pk_`, and `ek_` credentials cannot become management
@@ -226,6 +232,27 @@ Branch binding remains an authority boundary even when a key has no granular
226
232
  scope strings: a temporary child key can act only inside that child. It cannot
227
233
  manage siblings or gain root authority.
228
234
 
235
+ ### Cross-organization mint keys
236
+
237
+ Most applications do not need `ephemeral:mint-any-org`: their backend key mints
238
+ users into its own organization. A multi-organization backend needs it only
239
+ when each customer is a separate Ablo organization and one trusted service
240
+ mints sessions for all of them.
241
+
242
+ Treat that key as a dedicated minting credential:
243
+
244
+ - keep it in a server-side secret manager, never a browser or repository;
245
+ - grant only `ephemeral:mint-any-org`, with no data or schema scopes;
246
+ - mint short-lived sessions with the smallest typed `can` grant;
247
+ - rotate it on a schedule and revoke it immediately after suspected exposure;
248
+ - log the target `organizationId`, minted session id, and request id for audit.
249
+
250
+ The scope's broad name describes the cross-organization check it passes, not
251
+ the authority of each resulting session. The session can act only inside the
252
+ named customer organization and only for the models/verbs in `can`. See
253
+ [Customer Organizations](./customer-organizations.md) for the complete
254
+ integration.
255
+
229
256
  ## Current and legacy key spellings
230
257
 
231
258
  New credentials use one spelling per capability class:
package/docs/api.md CHANGED
@@ -173,7 +173,7 @@ The SDK is a convenience wrapper over a model-scoped HTTP surface — the same
173
173
  noun (`model`) and verbs as `ablo.<model>.…`. Non-JS callers (or curl) use it
174
174
  directly. The table below shows the shape with `{model}` as a placeholder; the
175
175
  [OpenAPI spec](./openapi.json) expands it into one **typed** path per model
176
- (`/api/v1/models/task`, `/api/v1/models/workspace`, …, generated from your schema) so each
176
+ (`/api/v1/models/record`, `/api/v1/models/workspace`, …, generated from your schema) so each
177
177
  endpoint documents that model's real field contract instead of a generic blob.
178
178
 
179
179
  | SDK call | HTTP |
package/docs/cli.md CHANGED
@@ -189,7 +189,7 @@ streams production. You never pass a project or branch. Follows by default;
189
189
 
190
190
  ```bash
191
191
  npx ablo logs # last 50, then stream
192
- npx ablo logs -n 100 --model task # backfill 100, one model
192
+ npx ablo logs -n 100 --model record # backfill 100, one model
193
193
  npx ablo logs --since 15m --json # last 15m as NDJSON, then stream
194
194
  ```
195
195
 
@@ -237,7 +237,7 @@ DATABASE_URL=postgres://… npx ablo check
237
237
  ```
238
238
 
239
239
  ```text
240
- taskstasks (id, organization_id ok)
240
+ recordsrecords (id, organization_id ok)
241
241
  ✗ projects → projects
242
242
  • missing "organization_id" — add it, or move this model behind a Data Source
243
243
  2 models · 1 ok · 1 error
@@ -308,7 +308,7 @@ that broke and the Postgres SQLSTATE, not just "migration failed".
308
308
  ```txt
309
309
  [migrate] migration plan failed {
310
310
  code: 'migration_failed',
311
- failedStatement: 'ALTER TABLE "public"."tasks" RENAME COLUMN a TO b;',
311
+ failedStatement: 'ALTER TABLE "public"."records" RENAME COLUMN a TO b;',
312
312
  failedStatementIndex: 4,
313
313
  pgCode: '42P01',
314
314
  durationMs: 133
@@ -13,7 +13,7 @@ you declared and nothing else.
13
13
  A plain write has no stale premise:
14
14
 
15
15
  ```ts
16
- await ablo.tasks.update({ id, data: { status: 'done' } });
16
+ await ablo.records.update({ id, data: { status: 'done' } });
17
17
  ```
18
18
 
19
19
  If no active claim conflicts with it, the write is last-write-wins. That is a
@@ -26,14 +26,14 @@ once in its `conflict` setting instead of at every call site.
26
26
  Pass the exact returned rows when a write is based on values previously read:
27
27
 
28
28
  ```ts
29
- const task = await ablo.tasks.get({ id });
29
+ const record = await ablo.records.get({ id });
30
30
  const policy = await ablo.policies.get({ id: policyId });
31
- if (!task || !policy) throw new Error('required input is missing');
31
+ if (!record || !policy) throw new Error('required input is missing');
32
32
 
33
- await ablo.tasks.update({
34
- id: task.id,
33
+ await ablo.records.update({
34
+ id: record.id,
35
35
  data: { status: 'done' },
36
- reads: [task, policy],
36
+ reads: [record, policy],
37
37
  });
38
38
  ```
39
39
 
@@ -83,7 +83,7 @@ See [Coordination](./coordination.md#claims) for the API.
83
83
  ## Cross-row and batch premises
84
84
 
85
85
  Model writes and lower-level commits can declare rows they read even when the
86
- write targets somewhere else. This protects decisions such as “update the task
86
+ write targets somewhere else. This protects decisions such as “update the record
87
87
  only if the deal I inspected has not changed.” A stale batch premise applies to
88
88
  the whole batch so atomicity is preserved.
89
89
 
package/docs/context.md CHANGED
@@ -21,13 +21,13 @@ import { generateText } from 'ai';
21
21
  const ctx = await context({
22
22
  ablo,
23
23
  data: {
24
- task: ablo.tasks.get({ id: taskId }),
25
- documents: ablo.documents.list({ where: { taskId } }),
26
- memory: loadMemories(taskId),
24
+ record: ablo.records.get({ id: recordId }),
25
+ records: ablo.records.list({ where: { recordId } }),
26
+ memory: loadMemories(recordId),
27
27
  },
28
28
  });
29
29
 
30
- if (!ctx.data.task) throw new Error('Task not found');
30
+ if (!ctx.data.record) throw new Error('Record not found');
31
31
 
32
32
  const result = await generateText({
33
33
  model,
@@ -35,8 +35,8 @@ const result = await generateText({
35
35
  tools,
36
36
  });
37
37
 
38
- await ablo.tasks.update({
39
- id: ctx.data.task.id,
38
+ await ablo.records.update({
39
+ id: ctx.data.record.id,
40
40
  data: parseTaskUpdate(result.text),
41
41
  reads: ctx.reads,
42
42
  });
@@ -80,7 +80,7 @@ This distinction is visible in `sources`:
80
80
  ```ts
81
81
  ctx.sources;
82
82
  // [
83
- // { key: 'task', kind: 'ablo', guarantee: 'guardable', cursor: 42 },
83
+ // { key: 'record', kind: 'ablo', guarantee: 'guardable', cursor: 42 },
84
84
  // { key: 'memory', kind: 'value', guarantee: 'informational', cursor: null },
85
85
  // ]
86
86
  ```
@@ -89,7 +89,7 @@ A top-level value may contain both kinds. It is then marked `mixed` and only
89
89
  its exact Ablo rows appear in `ctx.reads`:
90
90
 
91
91
  ```ts
92
- // data: { briefing: { task, memory } }
92
+ // data: { briefing: { record, memory } }
93
93
  // sources: [
94
94
  // { key: 'briefing', kind: 'mixed', guarantee: 'partial', cursor: 42 },
95
95
  // ]
@@ -108,10 +108,10 @@ Reducto, or another system behind their own interfaces.
108
108
  const ctx = await context({
109
109
  ablo,
110
110
  data: {
111
- task: ablo.tasks.get({ id: taskId }),
111
+ record: ablo.records.get({ id: recordId }),
112
112
  memory: loadMemories({ query, userId }),
113
113
  related: findRelatedChunks({ projectId, query }),
114
- evidence: extractEvidence({ documentId }),
114
+ evidence: extractEvidence({ recordId }),
115
115
  },
116
116
  });
117
117
  ```
@@ -141,7 +141,7 @@ await generateText({
141
141
  model,
142
142
  messages: [
143
143
  ...history,
144
- contextMessage(ctx, { include: ['task', 'documents', 'memory'] }),
144
+ contextMessage(ctx, { include: ['record', 'documents', 'memory'] }),
145
145
  ],
146
146
  tools,
147
147
  });
@@ -9,7 +9,7 @@ meaning. Choose the narrowest one that matches the operation.
9
9
  |---|---|---|
10
10
  | Set an independent value | `update({ id, data })` | Last-write-wins when no claim applies. |
11
11
  | Compute a value from the current row | `update(id, current => next)` | Re-reads and retries if the row changes concurrently. |
12
- | Write only if earlier rows are still current | `reads: [task, policy]` | Rejects when an explicitly named dependency changed. |
12
+ | Write only if earlier rows are still current | `reads: [record, policy]` | Rejects when an explicitly named dependency changed. |
13
13
  | Read, call a model, then write | `claim({ id })` | Other participants cannot write the claimed target by default until your claim ends. |
14
14
 
15
15
  **If a model call sits between the read and the write, take a claim.** A stale
@@ -25,16 +25,16 @@ does not carry a stale premise. It is intentionally last-write-wins.
25
25
  Pass the exact rows that produced a decision on the write:
26
26
 
27
27
  ```ts
28
- const task = await ablo.tasks.get({ id: taskId });
28
+ const record = await ablo.records.get({ id: recordId });
29
29
  const policy = await ablo.policies.get({ id: policyId });
30
- if (!task || !policy) throw new Error('required input is missing');
30
+ if (!record || !policy) throw new Error('required input is missing');
31
31
 
32
- const result = await model({ task, policy });
32
+ const result = await model({ record, policy });
33
33
 
34
- await ablo.tasks.update({
35
- id: task.id,
34
+ await ablo.records.update({
35
+ id: record.id,
36
36
  data: result,
37
- reads: [task, policy],
37
+ reads: [record, policy],
38
38
  });
39
39
  ```
40
40
 
@@ -57,7 +57,7 @@ When the next value is a function of the current one, pass an updater rather
57
57
  than fixed data:
58
58
 
59
59
  ```ts
60
- const document = await ablo.documents.update(documentId, (current) => ({
60
+ const document = await ablo.records.update(recordId, (current) => ({
61
61
  revision: current.revision + 1,
62
62
  content: revise(current.content),
63
63
  }));
@@ -75,8 +75,8 @@ effect inside it.
75
75
  You can bound or cancel reconciliation:
76
76
 
77
77
  ```ts
78
- await ablo.documents.update(
79
- documentId,
78
+ await ablo.records.update(
79
+ recordId,
80
80
  (current) => ({ revision: current.revision + 1 }),
81
81
  { retries: 8, signal: request.signal },
82
82
  );
@@ -184,8 +184,8 @@ protect the row version rather than a participant-held claim.
184
184
  For deduplicated jobs, skip work when another participant already owns it:
185
185
 
186
186
  ```ts
187
- const claim = await ablo.tasks.claim({
188
- id: taskId,
187
+ const claim = await ablo.records.claim({
188
+ id: recordId,
189
189
  contention: { mode: 'skip' },
190
190
  });
191
191
 
@@ -201,8 +201,8 @@ try {
201
201
  To wait with limits, keep the policy together:
202
202
 
203
203
  ```ts
204
- const claim = await ablo.tasks.claim({
205
- id: taskId,
204
+ const claim = await ablo.records.claim({
205
+ id: recordId,
206
206
  contention: {
207
207
  mode: 'wait',
208
208
  maxDepth: 3,
@@ -217,9 +217,9 @@ const claim = await ablo.tasks.claim({
217
217
  Narrow a claim when independent fields may be edited concurrently:
218
218
 
219
219
  ```ts
220
- await using claim = await ablo.tasks.claim({
221
- id: taskId,
222
- fields: (task) => task.status,
220
+ await using claim = await ablo.records.claim({
221
+ id: recordId,
222
+ fields: (record) => record.status,
223
223
  });
224
224
  ```
225
225
 
@@ -239,8 +239,8 @@ The target options are:
239
239
  Read current claim state without blocking:
240
240
 
241
241
  ```ts
242
- const holder = ablo.tasks.claim.state({ id: taskId });
243
- const queue = ablo.tasks.claim.queue({ id: taskId });
242
+ const holder = ablo.records.claim.state({ id: recordId });
243
+ const queue = ablo.records.claim.queue({ id: recordId });
244
244
  ```
245
245
 
246
246
  Use this state for presence and progress UI. Do not use an observed `null` as a