@abloatai/ablo 0.37.0 → 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.
@@ -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.
package/docs/migration.md CHANGED
@@ -214,10 +214,10 @@ commonly a Data Source handler keyed on `mode`. The mapping is exactly
214
214
  source resolvers, so per-project and per-environment traffic can be routed to
215
215
  distinct stores.
216
216
 
217
- > **CLI note:** the legacy single-file config that stored `test`/`live` key
218
- > buckets is no longer auto-migrated. If `ablo status` can't find your keys after
219
- > upgrading, re-run `ablo login` to write the current `sandbox`/`production`
220
- > layout.
217
+ > **CLI note:** current login stores one mode-free `mk_` project management
218
+ > credential. Legacy runtime key slots remain readable during the cutover, but
219
+ > re-run `ablo login` before using branch/project commands so the CLI can store
220
+ > the new credential.
221
221
 
222
222
  ### New (non-breaking): `transport: 'http'`
223
223
 
package/docs/projects.md CHANGED
@@ -30,7 +30,7 @@ projects existed are default-project keys automatically.
30
30
  ## Keys belong to exactly one project
31
31
 
32
32
  A key's project is fixed at mint and can never be changed or overridden —
33
- the same discipline as its sandbox/production mode. Everything a key mints
33
+ the same discipline as its immutable branch binding. Everything a runtime key mints
34
34
  inherits its project: the short-lived session credentials (`ek_`), agent
35
35
  keys (`rk_`), everything. There is no way to "switch projects" with an
36
36
  existing key; you use a key minted for the project you mean.
@@ -12,7 +12,7 @@ schema** — your migration tool stays in charge of the shape of your database.
12
12
 
13
13
  > No database yet? Pass an `apiKey` only and Ablo keeps your rows in its own log,
14
14
  > so you can build the whole app today — like Stripe test mode. Point it at a
15
- > separate or local Postgres for a throwaway sandbox, or at your production
15
+ > separate or local Postgres for a development branch, or at your production
16
16
  > database when you're ready.
17
17
 
18
18
  ## 1. Install and initialize
@@ -23,22 +23,23 @@ npx ablo init
23
23
  ```
24
24
 
25
25
  `ablo init` scaffolds your project (next step shows what it creates) and ends
26
- by signing you in — one browser click, and a `sk_test_` key is saved locally
27
- for the CLI. Later, `npx ablo push` (step 4) writes `ABLO_API_KEY` into your
28
- `.env.local` so the SDK finds it too — no manual copy-paste. `npx ablo login`
29
- also exists standalone. In CI, or to manage the key by hand, set it yourself
30
- instead:
26
+ by signing you in — one browser click, and a project-scoped `mk_` management
27
+ credential is saved locally. Later, `npx ablo dev` (step 4) prepares an isolated branch and writes
28
+ its temporary `ABLO_API_KEY` into `.env.local`, so the SDK follows your Git
29
+ branch with no manual copy-paste. `npx ablo login` also exists standalone. In
30
+ CI, set project management access explicitly:
31
31
 
32
32
  ```bash
33
- export ABLO_API_KEY=sk_test_...
33
+ export ABLO_MANAGEMENT_KEY=mk_...
34
34
  ```
35
35
 
36
- Every SDK and CLI call needs a key. Test and live keys work like Stripe's:
37
- `sk_test_*` for the sandbox, `sk_live_*` for production. In production a key
38
- points at the database *you* own; in the sandbox you can skip the database
39
- entirely and let Ablo's test plane host the rows (apiKey only). There is no
40
- keyless mode a key is always required. (The public `/sandbox` page is a
41
- separate hosted demo, not your app.)
36
+ Every runtime call needs a branch-bound API key. `ablo dev` exchanges the
37
+ stored management credential for
38
+ an expiring `sk_test_*` key bound to the current development branch.
39
+ Production runtimes use `sk_live_*`. In production a key points at the database
40
+ *you* own; on a development branch you can skip the database entirely and let
41
+ Ablo host the rows (apiKey only). There is no keyless mode — a key is always
42
+ required. (The public `/sandbox` page is a separate hosted demo, not your app.)
42
43
 
43
44
  ## 2. Your Ablo schema (init scaffolded it)
44
45
 
@@ -166,17 +167,24 @@ The full setup, the honest footprint (publication + slot + the `REPLICATION` and
166
167
  writer roles + the `wal_level` restart + slot/WAL retention Ablo monitors), and the
167
168
  Preview status are in [Connect Your Database](./data-sources.md).
168
169
 
169
- ## 4. Push the schema, then map it to tables
170
+ ## 4. Start the branch development loop
170
171
 
171
172
  ```bash
172
- npx ablo push # pushes the schema definition and writes ABLO_API_KEY to
173
- # .env.local. Add --watch to re-push on every save.
173
+ npx ablo dev # prepare this Git branch, push, and watch ablo/schema.ts
174
174
  ```
175
175
 
176
- `ablo push` uploads the schema *definition* model names, fields, types. That
177
- metadata is what tells Ablo which models to coordinate. Skipping it makes every
178
- write to a new or changed model fail with `server_execute_unknown_model` — that
179
- error literally means "run `npx ablo push`."
176
+ `ablo dev` discovers your Git branch, ensures a matching isolated Ablo branch,
177
+ mints an eight-hour branch credential, writes it to gitignored `.env.local`,
178
+ uploads the schema *definition*, and watches for schema edits. Model names,
179
+ fields, and types tell Ablo which models to coordinate. Skipping the push makes
180
+ every write to a new or changed model fail with
181
+ `server_execute_unknown_model`.
182
+
183
+ Use `npx ablo dev --no-watch` when you only need to prepare and push once. Use
184
+ `npx ablo push` as the lower-level one-shot command when you deliberately want
185
+ to push with the currently active key, including a reviewed production deploy.
186
+ The complete mental model and CI examples are in
187
+ [Branch-first development](./branch-development.md).
180
188
 
181
189
  Now map those models to your real Postgres tables. **Your migration tool owns the
182
190
  tables** — Ablo reads them, it does not create or migrate them:
@@ -191,8 +199,8 @@ tables** — Ablo reads them, it does not create or migrate them:
191
199
  > your schema needs. Once they exist, your own migration tool stays in charge
192
200
  > of them — Ablo adopts whatever shape you evolve.
193
201
 
194
- Nothing runs locally there is no dev server to start. Your app talks to Ablo's
195
- hosted API; the rows live in your database.
202
+ No Ablo server runs locally. The `ablo dev` process only watches your schema;
203
+ your app talks to Ablo's hosted API, and the rows live in your database.
196
204
 
197
205
  ## 5. Write through the model
198
206
 
package/docs/webhooks.md CHANGED
@@ -116,10 +116,10 @@ work asynchronously after responding.
116
116
 
117
117
  ## 2. Test locally
118
118
 
119
- You don't register a `localhost` URL. `npx ablo dev` forwards committed changes
120
- to your machine with a local signing secret the same idea as Stripe's
121
- `stripe listen`. Run your app, run `ablo dev`, and writes flow into your local
122
- handler.
119
+ `npx ablo dev` prepares an isolated schema branch; it does not forward webhook
120
+ deliveries. Register an HTTPS endpoint that Ablo can reach. For local handler
121
+ development, expose your app with the HTTPS tunnel your team already trusts,
122
+ then register that temporary URL and remove the endpoint when you finish.
123
123
 
124
124
  ## 3. Register your endpoint
125
125