@abloatai/ablo 0.37.1 → 0.38.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.
@@ -1,17 +1,14 @@
1
- # AI SDK Tool
1
+ # AI SDK Tools
2
2
 
3
- > Put a claim-and-commit loop inside an AI SDK tool call.
3
+ > Give an AI SDK agent safe access to the same typed Ablo resources as your
4
+ > backend.
4
5
 
5
6
  Use AI SDK for the agent loop and Ablo for the state boundary inside the tool.
6
7
  When an agent updates a shared record from inside a tool call you have a
7
- concurrency problem: another agent may be editing the same row, and a naive write
8
- silently overwrites it. This is the safe pattern read the record, claim the row
9
- so anyone else waits their turn, write through a checked update, and release the
10
- claim automatically.
11
-
12
- Claims don't lock. If another writer holds the row, `claim` waits for them,
13
- re-reads the fresh row, then hands it to you — so two writers serialize instead
14
- of clobbering.
8
+ concurrency problem: another agent or a person may be editing the same row, and
9
+ a naive write can overwrite work the model never saw. Ablo's tool adapters put
10
+ the authoritative read, retry, claim, and confirmed-write behavior behind the
11
+ ordinary AI SDK tool contract.
15
12
 
16
13
  ```ts
17
14
  // app/api/chat/route.ts
@@ -20,11 +17,11 @@ import { defineSchema, model, z as schemaZ } from '@abloatai/ablo/schema';
20
17
  import { anthropic } from '@ai-sdk/anthropic';
21
18
  import {
22
19
  streamText,
23
- tool,
24
20
  convertToModelMessages,
25
21
  stepCountIs,
26
22
  type UIMessage,
27
23
  } from 'ai';
24
+ import { updateTool } from '@abloatai/ablo/ai-sdk';
28
25
  import { z } from 'zod';
29
26
 
30
27
  export const runtime = 'nodejs';
@@ -43,42 +40,19 @@ const ablo = Ablo({
43
40
  transport: 'http',
44
41
  });
45
42
 
46
- const updateTask = tool({
47
- description: 'Update a task in the product database.',
43
+ const updateTask = updateTool(ablo.tasks, {
44
+ title: 'Update task',
45
+ description: 'Update a task without overwriting concurrent work.',
48
46
  inputSchema: z.object({
49
47
  taskId: z.string(),
50
48
  status: z.enum(['todo', 'doing', 'done']).optional(),
51
49
  summary: z.string().optional(),
52
50
  }),
53
- execute: async ({ taskId, status, summary }) => {
54
- await ablo.ready();
55
-
56
- // retrieve hits the server for the latest row (async — await it).
57
- const task = await ablo.tasks.get({ id: taskId });
58
- if (!task) return { ok: false, reason: 'not_found' };
59
-
60
- // If another agent already holds this row, claim waits for them to finish,
61
- // re-reads the fresh row, then hands it back on `claim.data`. The claim is
62
- // released automatically when it goes out of scope.
63
- await using claim = await ablo.tasks.claim({
64
- id: taskId,
65
- description: 'editing',
66
- ttl: '2m',
67
- });
68
-
69
- // Because you hold the claim, this update is rejected if the row changed
70
- // underneath you, instead of silently overwriting it.
71
- const updated = await ablo.tasks.update({
72
- id: claim.data.id,
73
- data: {
74
- status: status ?? claim.data.status,
75
- summary: summary ?? claim.data.summary,
76
- },
77
- wait: 'confirmed',
78
- });
79
-
80
- return { ok: true, task: updated };
81
- },
51
+ id: ({ taskId }) => taskId,
52
+ apply: (current, { status, summary }) => ({
53
+ status: status ?? current.status,
54
+ summary: summary ?? current.summary,
55
+ }),
82
56
  });
83
57
 
84
58
  export async function POST(req: Request) {
@@ -100,13 +74,12 @@ export async function POST(req: Request) {
100
74
 
101
75
  The model provider is interchangeable — swap `anthropic(...)` for any
102
76
  server-bound provider instance. What matters is that the route binds the model on
103
- the server (never trusting one sent in the request body), converts the incoming
104
- `UIMessage[]` with `convertToModelMessages`, and that the tool:
105
-
106
- - reads the latest row with `retrieve` (a server read),
107
- - claims it for exclusive, ordered access if someone else holds it, the claim
108
- waits for them, then re-reads,
109
- - writes through the model resource, which is rejected if the row changed
110
- underneath you,
111
- - waits for confirmation with `wait: 'confirmed'`,
112
- - and auto-releases the claim when the tool returns.
77
+ the server (never trusting one sent in the request body) and converts the
78
+ incoming `UIMessage[]` with `convertToModelMessages`.
79
+
80
+ `updateTool` defaults to a functional update: Ablo re-reads and reapplies the
81
+ patch if another participant writes first. Use `strategy: 'claim'` when the
82
+ model should skip work already owned by someone else, or `strategy: 'queue'`
83
+ when it should wait in Ablo's server-owned FIFO claim queue. The same entrypoint
84
+ also exports `readTool`, `createTool`, and `deleteTool`; deletes require AI SDK
85
+ approval unless the application explicitly disables it.
@@ -69,8 +69,9 @@ database differs by environment — the code is identical.
69
69
  - **Production:** your Postgres. `ablo connect` sets up a scoped writer role and
70
70
  logical replication; your rows live in your database, and Ablo writes to them
71
71
  through that role.
72
- - **Sandbox and local dev:** a separate or local Postgres you can throw away. Same
73
- models, same code, a different database behind them.
72
+ - **Development branches and local dev:** a separate or local Postgres, or a
73
+ branch of the one you already run. Same models, same code, a different
74
+ database behind them.
74
75
  - **Before you connect one.** Ablo keeps state in its own log, so you can build the
75
76
  whole app today and point it at a real database when you're ready.
76
77
 
package/docs/index.md CHANGED
@@ -60,11 +60,14 @@ based on a row that has since changed is turned away rather than applied.
60
60
  stay in your own migrations.
61
61
 
62
62
  ```bash
63
- npx ablo init && npx ablo push
63
+ npx ablo init
64
+ npx ablo dev
64
65
  ```
65
66
 
66
- `push` is the step everything depends on: the server keeps its own copy of the schema, and
67
- until it has yours, a write to a new model fails with `server_execute_unknown_model`.
67
+ `dev` gives the current Git branch an isolated Ablo branch, wires its temporary key,
68
+ pushes the schema, and watches for changes. Until the server has your schema, a write to
69
+ a new model fails with `server_execute_unknown_model`. See
70
+ [Branch-first development](./branch-development.md).
68
71
  </Step>
69
72
 
70
73
  <Step title="Connect the database the rows live in">
@@ -161,6 +164,7 @@ default caller, not a special one.
161
164
 
162
165
  - [Quickstart](./quickstart.md) — make your first coordinated write.
163
166
  - [Integration Guide](./integration-guide.md) — the canonical end-to-end integration.
167
+ - [Integrations](./integrations.md) — long-running tasks, ingestion, and other application-edge runtimes.
164
168
  - [CLI & Migrations](./cli.md) — `init` / `connect` / `push` / `migrate` / `generate`.
165
169
  - [Connect Your Database](./data-sources.md) — where rows land when your own database is canonical.
166
170
  - [Deployment](./deployment.md) — the database, the keys, and the schema push that take an integration to production.
@@ -66,24 +66,17 @@ deterministic demo; it does not call your API key or mutate hosted Ablo data.
66
66
  It is also built for coding agents: copy the sandbox prompt into Claude Code or
67
67
  Codex and ask it to wire one real model through the schema model API.
68
68
 
69
- Use the authenticated org dashboard sandbox for real integration work. The
70
- default sandbox is the equivalent of Stripe test mode:
71
-
72
- - it is scoped to the organization,
73
- - it has an isolated sync group prefix,
74
- - it mints `sk_test_*` keys,
75
- - it can be reset without touching live state,
76
- - additional sandboxes can start blank or from copied live configuration.
77
-
78
- Live keys and sandbox keys are separate. Use `sk_test_*` while wiring your app,
79
- agents, and Data Source endpoint; move to `sk_live_*` only when the same schema
80
- and write path are ready for production.
69
+ Use `npx ablo dev` for real integration work. It derives an immutable branch
70
+ from Git, inherits the parent schema, and writes a temporary branch credential
71
+ to `.env.local`. Each developer or pull request gets independent schema, rows,
72
+ claims, and logs. Use an explicit `sk_live_*` root credential only in the
73
+ reviewed production deployment.
81
74
 
82
75
  When handing this to a coding agent, give it a concrete target:
83
76
 
84
77
  ```txt
85
78
  Add Ablo to this app for one model your agents edit.
86
- Use the org sandbox sk_test_* key. Declare schema, add the Ablo client, replace
79
+ Run npx ablo dev and use its branch-bound key. Declare schema, add the Ablo client, replace
87
80
  one write with ablo.<model>.update(..., { readAt, onStale: 'reject',
88
81
  wait: 'confirmed' }), and add a smoke test for two concurrent writers.
89
82
  ```
@@ -0,0 +1,258 @@
1
+ # Inngest for long-running tasks
2
+
3
+ > Run event-driven, retryable agent tasks with Inngest while Ablo makes each
4
+ > shared-state effect typed, idempotent, and authoritative.
5
+
6
+ Inngest and Ablo solve different parts of a durable agent system:
7
+
8
+ | Concern | Owner |
9
+ |---|---|
10
+ | Events, step checkpoints, retries, sleeps, flow control, cancellation | Inngest |
11
+ | Provider calls, prompts, and model output | AI SDK |
12
+ | Typed reads and writes, mutation idempotency, claims, confirmation | Ablo |
13
+ | Event names, function definitions, effect identity, business behavior | Your application |
14
+
15
+ The short version is:
16
+
17
+ > Inngest makes sure the job resumes. Ablo makes sure its changes are safe.
18
+
19
+ ## Keep Ablo calls inside steps
20
+
21
+ An Inngest function is ordinary application code, but external I/O belongs in
22
+ a retriable `step.run()`. A successful step result is checkpointed and reused;
23
+ a failed step is independently retried.
24
+
25
+ ```ts
26
+ import Ablo from '@abloatai/ablo';
27
+ import { Inngest, eventType } from 'inngest';
28
+ import { z } from 'zod';
29
+ import { schema } from './schema.js';
30
+
31
+ const inngest = new Inngest({ id: 'orders' });
32
+ const ablo = Ablo({
33
+ schema,
34
+ apiKey: process.env.ABLO_API_KEY,
35
+ transport: 'http',
36
+ });
37
+
38
+ const approvalRequested = eventType('orders/approval.requested', {
39
+ schema: z.object({
40
+ operationId: z.string(),
41
+ orderId: z.string(),
42
+ approvalNote: z.string(),
43
+ }),
44
+ });
45
+
46
+ export const approveOrder = inngest.createFunction(
47
+ {
48
+ id: 'approve-order',
49
+ triggers: [approvalRequested],
50
+ retries: 4,
51
+ idempotency: 'event.data.operationId',
52
+ },
53
+ async ({ event, step }) => {
54
+ const { operationId, orderId, approvalNote } = event.data;
55
+
56
+ return step.run('write-approved-order', () =>
57
+ ablo.orders.update({
58
+ id: orderId,
59
+ data: { status: 'approved', approvalNote },
60
+ idempotencyKey: `${operationId}:approve-order:${orderId}`,
61
+ wait: 'confirmed',
62
+ }),
63
+ );
64
+ },
65
+ );
66
+ ```
67
+
68
+ There is no Inngest-specific Ablo transport. The step calls the same branded
69
+ model API as a route handler, worker, or command-line process.
70
+
71
+ ## Use both idempotency layers
72
+
73
+ Send a globally scoped event ID and carry a stable business operation ID in the
74
+ event:
75
+
76
+ ```ts
77
+ await inngest.send(
78
+ approvalRequested.create(
79
+ {
80
+ operationId,
81
+ orderId,
82
+ approvalNote: 'Approved by the durable function.',
83
+ },
84
+ {
85
+ id: `orders-approval-requested-${operationId}`,
86
+ },
87
+ ),
88
+ );
89
+ ```
90
+
91
+ Inngest event IDs and function-level `idempotency` prevent duplicate function
92
+ runs for Inngest's documented 24-hour window. They do not replace the Ablo
93
+ mutation key. Ablo can confirm the write and the process can lose the response
94
+ before Inngest checkpoints the step, causing the step callback to execute
95
+ again.
96
+
97
+ For every mutating step:
98
+
99
+ 1. Accept a stable business operation ID in the triggering event.
100
+ 2. Derive a separate key for each logical effect.
101
+ 3. Reuse the key and mutation body for every retry.
102
+ 4. Use `wait: 'confirmed'` when later steps depend on the authoritative result.
103
+ 5. Treat reuse of a key with another body as an application bug.
104
+ 6. Keep the complete retry horizon within Ablo's documented
105
+ [idempotency retention window](../idempotency.md).
106
+
107
+ Never put Inngest's `attempt`, a timestamp, or a random value in the key.
108
+ `runId` is suitable only when a newly created function run should represent a
109
+ new effect.
110
+
111
+ ## Compose AI SDK as durable steps
112
+
113
+ Inngest can checkpoint AI SDK calls with `step.ai.wrap()`. Keep the model call
114
+ and the Ablo mutation as separate steps:
115
+
116
+ ```ts
117
+ import { openai } from '@ai-sdk/openai';
118
+ import { generateText } from 'ai';
119
+
120
+ const { text: approvalNote } = await step.ai.wrap(
121
+ 'review-order',
122
+ generateText,
123
+ {
124
+ model: openai('gpt-4o-mini'),
125
+ system:
126
+ 'Return only a concise approval note suitable for an audit trail.',
127
+ prompt,
128
+ },
129
+ );
130
+
131
+ await step.run('apply-order-review', () =>
132
+ ablo.orders.update({
133
+ id: orderId,
134
+ data: { status: 'approved', approvalNote },
135
+ idempotencyKey: `${operationId}:apply-order-review:${orderId}`,
136
+ wait: 'confirmed',
137
+ }),
138
+ );
139
+ ```
140
+
141
+ This separation means retrying a write does not spend model tokens again, and
142
+ retrying a model call cannot partially mutate shared state.
143
+
144
+ For a multi-turn agent loop, checkpoint every model invocation and wrap every
145
+ tool execution in its own `step.run()`. The generic
146
+ `@abloatai/ablo/ai-sdk` helpers may execute inside a step because Inngest does
147
+ not impose Temporal's deterministic Workflow sandbox. Explicit model calls are
148
+ still preferable in the first integration because they make the effect key and
149
+ step boundary visible.
150
+
151
+ ## Claims and cancellation
152
+
153
+ A claim protects a short shared-state critical section, not an entire Inngest
154
+ function. Acquire, fresh-read, checked-write, and release inside one
155
+ `step.run()`:
156
+
157
+ ```ts
158
+ await step.run('apply-review-with-claim', async () => {
159
+ const claim = await ablo.orders.claim({
160
+ id: orderId,
161
+ description: 'applying an Inngest review',
162
+ });
163
+
164
+ try {
165
+ return await ablo.orders.update({
166
+ id: claim.data.id,
167
+ data: { status: 'approved', approvalNote },
168
+ claim,
169
+ idempotencyKey: `${operationId}:apply-review:${orderId}`,
170
+ wait: 'confirmed',
171
+ });
172
+ } finally {
173
+ await claim.release();
174
+ }
175
+ });
176
+ ```
177
+
178
+ Do not hold a claim across `step.sleep()`, `step.waitForEvent()`, or an AI
179
+ inference step. Configure `cancelOn` for the function, but do not assume that
180
+ function cancellation aborts an already running Ablo network request. Keep
181
+ claims short, release in `finally`, and rely on bounded claim leases for
182
+ process-loss recovery.
183
+
184
+ ## Expose the application endpoint
185
+
186
+ Mount Inngest's handler at the conventional `/api/inngest` path. For a plain
187
+ Node application:
188
+
189
+ ```ts
190
+ import { createServer } from 'node:http';
191
+ import { serve } from 'inngest/node';
192
+
193
+ const handler = serve({
194
+ client: inngest,
195
+ functions: [approveOrder],
196
+ });
197
+
198
+ createServer((request, response) => {
199
+ const path = new URL(request.url ?? '/', 'http://localhost').pathname;
200
+ if (path === '/api/inngest') return handler(request, response);
201
+ response.writeHead(404).end();
202
+ }).listen(3000);
203
+ ```
204
+
205
+ Use the framework-specific `serve` adapter in an existing Next.js, Express,
206
+ Hono, or other supported application.
207
+
208
+ Start the local app and Inngest Dev Server:
209
+
210
+ ```bash
211
+ npm run dev
212
+ npx --ignore-scripts=false inngest-cli@latest dev \
213
+ --no-discovery \
214
+ -u http://localhost:3000/api/inngest
215
+ ```
216
+
217
+ Set `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY` in production.
218
+
219
+ ## Test the failure boundary
220
+
221
+ Use `@inngest/test`'s `InngestTestEngine` for function and step tests. A useful
222
+ integration suite proves:
223
+
224
+ - the Ablo write happens inside the expected named step;
225
+ - a confirmed write followed by simulated response loss retries with the same
226
+ key and body and commits once;
227
+ - conflicting reuse of a key fails;
228
+ - checkpointed step state bypasses the Ablo callback;
229
+ - claims release after write failure; and
230
+ - the `/api/inngest` endpoint exposes function metadata without eagerly
231
+ creating an Ablo connection.
232
+
233
+ The complete runnable suite lives in `examples/inngest-agent`.
234
+
235
+ ## Package boundary
236
+
237
+ Inngest remains an application dependency:
238
+
239
+ ```text
240
+ packages/
241
+ transaction/src/ai-sdk/ reusable model-backed AI SDK tools
242
+ ablo/src/ai-sdk.ts @abloatai/ablo/ai-sdk
243
+
244
+ examples/
245
+ inngest-agent/ events, functions, steps, endpoint, AI composition
246
+ ```
247
+
248
+ Do not add Inngest to `packages/agent`. A dedicated `@abloatai/inngest` package
249
+ is justified only after multiple real applications reveal substantial
250
+ reusable behavior beyond a small function or step wrapper.
251
+
252
+ ## References
253
+
254
+ - [Inngest TypeScript SDK v4](https://www.inngest.com/docs/reference/typescript/v4/intro)
255
+ - [Inngest steps](https://www.inngest.com/docs/reference/typescript/functions/step-run)
256
+ - [Inngest idempotency](https://www.inngest.com/docs/guides/handling-idempotency)
257
+ - [Inngest AI inference steps](https://www.inngest.com/docs/features/inngest-functions/steps-workflows/step-ai-orchestration)
258
+ - [Testing Inngest functions](https://www.inngest.com/docs/reference/typescript/v4/testing)
@@ -0,0 +1,187 @@
1
+ # Temporal for long-running tasks
2
+
3
+ > Run long-lived, retryable agent tasks with Temporal while Ablo makes each
4
+ > shared-state effect typed, idempotent, and authoritative.
5
+
6
+ Temporal and Ablo solve different parts of a durable agent system:
7
+
8
+ | Concern | Owner |
9
+ |---|---|
10
+ | Workflow history, replay, timers, retries, cancellation | Temporal |
11
+ | Provider, messages, tools, approval, model loop | AI SDK |
12
+ | Typed reads and writes, idempotency, claims, confirmation | Ablo |
13
+ | Workflow names, task queues, retry policy, business behavior | Your application |
14
+
15
+ The short version is:
16
+
17
+ > Temporal makes sure the agent finishes. Ablo makes sure its changes are safe.
18
+
19
+ ## Keep Ablo calls in Activities
20
+
21
+ Temporal Workflow code must be deterministic. An Ablo model operation performs
22
+ network I/O, so instantiate `Ablo()` and call model resources in an Activity:
23
+
24
+ ```ts
25
+ // activities/orders.ts
26
+ import Ablo from '@abloatai/ablo';
27
+ import { schema } from '../schema.js';
28
+
29
+ const ablo = Ablo({
30
+ schema,
31
+ apiKey: process.env.ABLO_API_KEY,
32
+ transport: 'http',
33
+ });
34
+
35
+ export interface ApproveOrderInput {
36
+ orderId: string;
37
+ approvalNote: string;
38
+ idempotencyKey: string;
39
+ }
40
+
41
+ export async function approveOrder(input: ApproveOrderInput) {
42
+ return ablo.orders.update({
43
+ id: input.orderId,
44
+ data: {
45
+ status: 'approved',
46
+ approvalNote: input.approvalNote,
47
+ },
48
+ idempotencyKey: input.idempotencyKey,
49
+ wait: 'confirmed',
50
+ });
51
+ }
52
+ ```
53
+
54
+ The Activity uses the same branded model method as a route handler, job, or
55
+ command-line process. There is no Temporal-specific Ablo transport.
56
+
57
+ Workflow code creates the stable logical effect identity and schedules the
58
+ Activity:
59
+
60
+ ```ts
61
+ // workflows/approve-order.ts
62
+ import { proxyActivities, workflowInfo } from '@temporalio/workflow';
63
+ import type * as activities from '../activities/orders.js';
64
+
65
+ const { approveOrder } = proxyActivities<typeof activities>({
66
+ startToCloseTimeout: '1 minute',
67
+ retry: { maximumAttempts: 5 },
68
+ });
69
+
70
+ export async function approveOrderWorkflow(orderId: string) {
71
+ const { workflowId, runId } = workflowInfo();
72
+
73
+ return approveOrder({
74
+ orderId,
75
+ approvalNote: 'Approved by the durable workflow.',
76
+ idempotencyKey: `${workflowId}:${runId}:approve-order:${orderId}`,
77
+ });
78
+ }
79
+ ```
80
+
81
+ Temporal may retry an Activity after the Ablo write succeeded but before the
82
+ result reached Temporal. Every attempt must therefore send the same
83
+ `idempotencyKey` and the same mutation body. Do not put an Activity attempt
84
+ number, timestamp, or random value in the key.
85
+
86
+ Using the Workflow Run ID is appropriate when a new run should represent a new
87
+ effect. If an effect must survive Continue-As-New or a newly started Workflow
88
+ run, accept a stable business operation ID as Workflow input instead.
89
+
90
+ ## Compose AI SDK tools through Activities
91
+
92
+ Temporal's AI SDK integration can make the model interaction durable inside a
93
+ Workflow. Any tool that touches Ablo still delegates to an Activity:
94
+
95
+ ```ts
96
+ import { temporalProvider } from '@temporalio/ai-sdk/workflow';
97
+ import { generateText, stepCountIs, tool } from 'ai';
98
+ import { z } from 'zod';
99
+
100
+ const result = await generateText({
101
+ model: temporalProvider.languageModel('gpt-4o-mini'),
102
+ prompt,
103
+ tools: {
104
+ approveOrder: tool({
105
+ description: 'Approve the order after reviewing it.',
106
+ inputSchema: z.object({ approvalNote: z.string() }),
107
+ execute: ({ approvalNote }) =>
108
+ approveOrder({
109
+ orderId,
110
+ approvalNote,
111
+ idempotencyKey: `${effectPrefix}:approve-order:${orderId}`,
112
+ }),
113
+ }),
114
+ },
115
+ stopWhen: stepCountIs(5),
116
+ });
117
+ ```
118
+
119
+ Register Temporal's `AiSdkPlugin` on the Worker and pin all
120
+ `@temporalio/*` packages to one compatible version set. The integration is
121
+ experimental, so replay-test Workflow histories before upgrading it.
122
+ The import above matches the example's pinned `1.21.1` release. Follow the
123
+ matching Temporal upgrade guide when changing that version set.
124
+
125
+ Do not import `@abloatai/ablo/ai-sdk` into Workflow code. Its `readTool`,
126
+ `createTool`, `updateTool`, and `deleteTool` helpers execute Ablo model
127
+ operations directly and are intended for ordinary Node.js AI SDK loops. In a
128
+ Temporal Workflow, define the tool at the application edge and make its
129
+ `execute` function call a proxied Activity.
130
+
131
+ ## Idempotency rules
132
+
133
+ For every mutating Activity:
134
+
135
+ 1. Create the key in deterministic Workflow code or accept a stable operation
136
+ ID in the Workflow input.
137
+ 2. Reuse the key and mutation body for every retry of the same logical effect.
138
+ 3. Use `wait: 'confirmed'` when later Workflow steps depend on the
139
+ authoritative database result.
140
+ 4. Treat reuse of a key with a different body as an application bug.
141
+ 5. Keep the Temporal retry horizon within Ablo's documented
142
+ [idempotency retention window](../idempotency.md).
143
+
144
+ This gives effective-once composition for the advertised retention window. It
145
+ does not make an external side effect physically execute only once; Temporal
146
+ Activities are intentionally retryable.
147
+
148
+ ## Claims and cancellation
149
+
150
+ A claim protects a short shared-state operation, not an entire Workflow
151
+ history. Keep claim acquisition, the fresh read, the checked write, and release
152
+ inside one Activity:
153
+
154
+ ```ts
155
+ import { Context } from '@temporalio/activity';
156
+
157
+ export async function applyReview(orderId: string, note: string) {
158
+ await using claim = await ablo.orders.claim({
159
+ id: orderId,
160
+ description: 'applying review',
161
+ signal: Context.current().cancellationSignal,
162
+ });
163
+
164
+ return ablo.orders.update({
165
+ id: claim.data.id,
166
+ data: { approvalNote: note },
167
+ claim,
168
+ wait: 'confirmed',
169
+ });
170
+ }
171
+ ```
172
+
173
+ For long model reasoning, reason in the Workflow and use a short Activity to
174
+ re-read and apply the result. Do not return from an Activity while assuming a
175
+ held lease will remain valid.
176
+
177
+ ## Complete example
178
+
179
+ The repository's
180
+ [`examples/temporal-agent`](../../../../examples/temporal-agent/README.md)
181
+ contains a Workflow, Activities, Worker, client, durable AI SDK tool, and a
182
+ simulated lost-response retry. It is a standalone application on purpose:
183
+ Temporal stays out of Ablo's core packages and out of `packages/agent`.
184
+
185
+ A dedicated `@abloatai/temporal` package should be introduced only after
186
+ multiple production integrations reveal substantial, stable behavior that
187
+ cannot be expressed clearly at this application boundary.
@@ -0,0 +1,54 @@
1
+ # Integrations
2
+
3
+ > Compose Ablo's authoritative shared-state operations with the runtimes that
4
+ > execute, schedule, or ingest work in your application.
5
+
6
+ Integrations live at the application edge. Ablo continues to own typed reads
7
+ and writes, idempotency, claims, and authoritative confirmation; the external
8
+ runtime keeps owning the job it was designed for.
9
+
10
+ | Category | Integration | Status | Use it for |
11
+ |---|---|---|---|
12
+ | Long-running tasks | [Temporal](./integrations/temporal.md) | Available | Durable Workflows, Activity retries, timers, cancellation, and durable AI SDK calls |
13
+ | Long-running tasks | [Inngest](./integrations/inngest.md) | Available | Event-driven durable functions, retriable steps, flow control, and checkpointed AI SDK calls |
14
+ | Data ingestion | Connector runtimes | Planned | Bringing external data into Ablo-backed models without creating a second write authority |
15
+
16
+ An integration gets its own guide when there is runnable application code and
17
+ the boundary has been tested. A dedicated package comes later still: only
18
+ repeated production integrations that reveal substantial reusable behavior
19
+ justify adding another public runtime dependency.
20
+
21
+ ## Long-running tasks
22
+
23
+ Use [Temporal](./integrations/temporal.md) when work must survive process
24
+ failure, retry Activities, wait on timers, or preserve a durable model loop.
25
+ Temporal owns execution history. Each tool or Activity that touches shared
26
+ state calls the branded Ablo model API at the application edge.
27
+
28
+ The complete example lives in `examples/temporal-agent` and includes retry,
29
+ replay, cancellation, claim release, and Workflow-bundle tests.
30
+
31
+ Use [Inngest](./integrations/inngest.md) when durable work starts from events
32
+ and should be expressed as independently retriable steps behind an application
33
+ HTTP endpoint. Inngest owns event delivery, step checkpoints, and flow control.
34
+ Each `step.run()` that touches shared state calls the same branded Ablo model
35
+ API used elsewhere in the application.
36
+
37
+ The complete example lives in `examples/inngest-agent` and includes
38
+ confirmed-write response-loss replay, key/body conflict, checkpoint reuse,
39
+ claim cleanup, and endpoint-discovery tests.
40
+
41
+ ## Data ingestion
42
+
43
+ Data ingestion is separate from the Inngest durable-execution integration. A
44
+ connector or ingestion runtime belongs in this section when its public contract
45
+ exists. The same ownership rule will apply:
46
+
47
+ - the ingestion runtime owns connectors, polling, checkpoints, parsing, and
48
+ backpressure;
49
+ - Ablo owns validated model writes, idempotency, coordination, and confirmed
50
+ outcomes;
51
+ - the consuming application owns field mapping and business policy.
52
+
53
+ Until that contract and a runnable example exist, this page records the
54
+ category without publishing placeholder imports or configuration.