@lotics/app-sdk 0.77.1 → 0.77.3
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 +2 -2
- package/docs/ai.md +43 -31
- package/docs/data_fetching.md +6 -8
- package/docs/files.md +5 -6
- package/docs/mutations.md +1 -4
- package/docs/navigation_and_state.md +1 -2
- package/docs/queries.md +17 -37
- package/docs/recipes.md +2 -3
- package/docs/runtime.md +3 -8
- package/docs/workflows.md +24 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -14,7 +14,7 @@ signature; open the file.**
|
|
|
14
14
|
|
|
15
15
|
| Doc | Read it for |
|
|
16
16
|
|---|---|
|
|
17
|
-
| [docs/recipes.md](./docs/recipes.md) | Task-shaped how-tos for the actions whose mechanism is not guessable from the hooks — returning a generated file, returning structured data, parameterized lookups, composable optional filters, cell decoding, testing an AI action without spending credits.
|
|
17
|
+
| [docs/recipes.md](./docs/recipes.md) | Task-shaped how-tos for the actions whose mechanism is not guessable from the hooks — returning a generated file, returning structured data, parameterized lookups, composable optional filters, cell decoding, testing an AI action without spending credits. |
|
|
18
18
|
| [docs/queries.md](./docs/queries.md) | **The query engine authoring reference** — AST node kinds, per-field-type operator support, filters/params/pruning, free-text search, combining tables (join/union/link/`unnest`/`record_id`), shaping (aggregates, date buckets, windows), runtime refinement bounds, limits & the efficiency playbook. |
|
|
19
19
|
| [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), the `QueryRow` shape (projected columns `unknown`; `__source_record_id`/`__source_table_id` typed but optional), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, caching — **arrival revalidates** (a re-mount renders cache *and* refreshes it in the background, `loading` never flips) — data discipline, the pagination count as a second full execution (and `total` to suppress it), the search-as-you-type + record-picker patterns. |
|
|
20
20
|
| [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract, typed inputs, diff-before-update, locked records, `useOptimistic`, `useNewRecord` (client-minted `rec_*` id so a new-record surface never remounts on its first save), read-after-write ordering (a re-read must not overtake an in-flight write). |
|
|
@@ -22,7 +22,7 @@ signature; open the file.**
|
|
|
22
22
|
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs (**a bearer credential for the bytes** — never logged, reported, or persisted), workflow-generated files, **naming a zip's entries** (`{ id, name }` per file — a file name, never a path), preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
|
|
23
23
|
| [docs/members_and_options.md](./docs/members_and_options.md) | People + select options + comments — `useMembers`, `useFieldOptions`, `useViewer`, `useComments`, and the `@lotics/ui` components they feed. |
|
|
24
24
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
25
|
-
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics, auto query-refetch on chat mutation). **A `file` input carries its own content** —
|
|
25
|
+
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state — and the `AgentRunLanding` every leg resolves), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics, auto query-refetch on chat mutation). **A `file` input carries its own content** — no reader tool to declare. **An agent reaches record DATA only through its declared `query_aliases` / `workflow_aliases`.** Also **what the member's own chat agent can do with your app while it is open** — the alias catalog it reads and how to shape a mutating alias for it. |
|
|
26
26
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, what runtime refinement cannot widen, and why a per-input bound is a tenancy floor rather than an authorization check (a caller-supplied id must be intersected with the record server-side). |
|
|
27
27
|
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, `openExternal`/`downloadFile`, geofencing, analytics, `useConfig` (App-Packages installation config), `getAppBinding` (package apps' runtime `F`/`OPT`/`ROLE` resolution via the generated `.lotics/app_fields.ts`), and the publish chain for package contributors. |
|
|
28
28
|
|
package/docs/ai.md
CHANGED
|
@@ -26,9 +26,9 @@ A declaration carries:
|
|
|
26
26
|
| `workflow_aliases` | The app's own workflows the agent may invoke via `run_app_workflow` — its **entire write surface** |
|
|
27
27
|
| `model_tier` | Optional model tier — `haiku` \| `sonnet` \| `opus`. Omit (preferred) to follow the platform default tier, resolved at run time. A tier names capability, not a version, so the agent tracks model generations with no rewrite. Pin only a deliberate, tested choice |
|
|
28
28
|
| `effort_level` | Optional reasoning depth for adaptive-thinking tiers. Requires an explicit `model_tier` pin — effort is tuned per tier |
|
|
29
|
-
| `prefix_cache_ttl` | Optional prompt-cache window for the agent's stable prefix (tools + system). **Omit it** — the default (`"5m"`, Anthropic's own) is right for essentially every agent. `"1h"` is a leveraged bet: it doubles the write price (2x the input rate against 1.25x, both reading back at 0.1x) to buy only the five-minute-to-one-hour band
|
|
29
|
+
| `prefix_cache_ttl` | Optional prompt-cache window for the agent's stable prefix (tools + system). **Omit it** — the default (`"5m"`, Anthropic's own) is right for essentially every agent. `"1h"` is a leveraged bet: it doubles the write price (2x the input rate against 1.25x, both reading back at 0.1x) to buy only the five-minute-to-one-hour band. Declining it is never "uncached" — the same prefix stays cached at the default window. Set `"1h"` only with measured cadence showing runs reliably land in that band, such as a scheduled sweep |
|
|
30
30
|
| `inputs` | Optional typed input schema for one run — the same vocabulary as workflow inputs (`text`, `number`, `file`, `member`, `record_link`, `select`, …). The server validates every run payload against it. **Every field defaults to `required: true`**, exactly as `outputs` does — the two extend the same base — so an input the caller may legitimately omit needs `"required": false`, or the run is rejected before the agent sees it |
|
|
31
|
-
| `outputs` | Optional typed output schema. Declared → **structured agent** (the run must emit a matching result); omitted → **free-text agent** (the answer is the final prose). **Every field defaults to `required: true`** (the same base schema as `inputs` above) — mark `"required": false` on anything the source may legitimately not carry. It matters most for `number`, which has no blank: text can answer `""`, but a required number leaves only a wrong value or a rejected submission
|
|
31
|
+
| `outputs` | Optional typed output schema. Declared → **structured agent** (the run must emit a matching result); omitted → **free-text agent** (the answer is the final prose). **Every field defaults to `required: true`** (the same base schema as `inputs` above) — mark `"required": false` on anything the source may legitimately not carry. It matters most for `number`, which has no blank: text can answer `""`, but a required number leaves only a wrong value or a rejected submission |
|
|
32
32
|
|
|
33
33
|
**Structured vs free-text is the load-bearing split.** A structured agent's result arrives in `run.output` (the server strictly validates the submitted result against the declared schema — see the `output` typing section for the exact guarantee); a free-text agent's answer is the transcript's prose (`run.text`) and its `output` stays `undefined` — never a stray string, so a consumer reading `output.<field>` can't crash on a free-text answer.
|
|
34
34
|
|
|
@@ -198,7 +198,7 @@ On recovery, `output` adopts the row's output **only when it is an object** (a s
|
|
|
198
198
|
|
|
199
199
|
- **The app owns the state.** Always pass the authoritative current state in `input` — the session context is memory, not the source of truth.
|
|
200
200
|
- **Mint a new `sessionId` to clear context.** There is no reset call; a fresh key is a fresh session.
|
|
201
|
-
- **One-shot agents get a fresh `sessionId` per run.** Session context only pays for conversational follow-ups. If every run is self-contained (the app passes the complete input each time — e.g. a document-extraction agent), reusing a key replays dead context into every request — every replayed run still bills its input tokens, so a one-shot agent on a shared key pays for history it never uses. Append a per-run nonce to the key instead. (Media itself replays as a provider file reference — uploaded once, referenced by id — so
|
|
201
|
+
- **One-shot agents get a fresh `sessionId` per run.** Session context only pays for conversational follow-ups. If every run is self-contained (the app passes the complete input each time — e.g. a document-extraction agent), reusing a key replays dead context into every request — every replayed run still bills its input tokens, so a one-shot agent on a shared key pays for history it never uses. Append a per-run nonce to the key instead. (Media itself replays as a provider file reference — uploaded once, referenced by id — so the cost of dead context is tokens, not payload.)
|
|
202
202
|
|
|
203
203
|
Sessions are scoped to the authenticated member who ran them: two members using the same `sessionId` string share nothing, and a member can never read or extend another member's thread.
|
|
204
204
|
|
|
@@ -211,15 +211,14 @@ Sessions are scoped to the authenticated member who ran them: two members using
|
|
|
211
211
|
| **Perceived natively** | `image/*`, `application/pdf` | A vision / document part — the agent literally sees it |
|
|
212
212
|
| **Materialized** | Word (`.docx`/`.doc`), Excel (`.xlsx`/`.xls`), CSV, and text (`.txt`, `.md`, `.json`, `.eml`, `.html`, `.xml`, `.yaml`) | Read server-side by the same engines `view_files` uses and inlined into the run's message, truncated at 40,000 characters with the agent told when that happened |
|
|
213
213
|
|
|
214
|
-
**Pictures inside a Word document are delivered as images**, so a scanned page, an ID card photographed into a `.docx`, or a screenshot pasted into one is READ, not merely described.
|
|
214
|
+
**Pictures inside a Word document are delivered as images**, so a scanned page, an ID card photographed into a `.docx`, or a screenshot pasted into one is READ, not merely described. Any picture that is skipped or unresolvable is counted and stated in the text, so a partially-read document never reads as a complete one.
|
|
215
215
|
| **Unreadable** | Archives, audio, video | The run fails immediately, naming the file — a fact about the upload, not a gap in your configuration |
|
|
216
216
|
|
|
217
217
|
Every file in a `multi` input is materialized — nothing is collapsed to the first. Get the ids from [`useFileUpload` / `useAttachments`](./files.md).
|
|
218
218
|
|
|
219
219
|
**So do not dispatch on the file type.** Every tier above reads, which means the format tells you
|
|
220
220
|
nothing about what a document IS — the same claim form arrives as `.xlsx` from a laptop, as a PDF
|
|
221
|
-
through a chat app, and as a photo of a printout
|
|
222
|
-
or as a screenshot. An intake that routes on the extension is reading the envelope, and it fails
|
|
221
|
+
through a chat app, and as a photo of a printout. An intake that routes on the extension is reading the envelope, and it fails
|
|
223
222
|
SILENTLY: the wrong extractor runs, returns something correctly shaped, and the user is handed a
|
|
224
223
|
confident wrong answer. Let one agent read the drop and say what each document is, keyed on what
|
|
225
224
|
only the content carries. Prefer separate output slots (`don_hang`, `hoa_don`, plus a named
|
|
@@ -333,7 +332,7 @@ Declarative and lifecycle-bound: mounting or changing `context` pushes it; unmou
|
|
|
333
332
|
| `data` | must JSON-serialize to ≤ 2000 chars, else it is **replaced by an `omitted` marker** the agent reads (the description is kept) |
|
|
334
333
|
| slots per app | ≤ 8 (a 9th evicts the least-recently-updated) |
|
|
335
334
|
|
|
336
|
-
Keep `description` tight and human-readable — it is the line the agent reads. Put anything structured (filter/sort/form values) in `data`, and remember it is replaced whole past its size cap: the agent is told the state existed but not what it was, so don't hide load-bearing facts there that aren't also in `description`.
|
|
335
|
+
Keep `description` tight and human-readable — it is the line the agent reads. Put anything structured (filter/sort/form values) in `data`, and remember it is replaced whole past its size cap: the agent is told the state existed but not what it was, so don't hide load-bearing facts there that aren't also in `description`.
|
|
337
336
|
|
|
338
337
|
### Security — push-only, a snapshot of what was already rendered
|
|
339
338
|
|
|
@@ -346,7 +345,7 @@ This hook is a **one-way push of data the app already showed this member**. (Cha
|
|
|
346
345
|
|
|
347
346
|
`useAiContext` is a PUSH — a snapshot of what one screen rendered. It cannot
|
|
348
347
|
answer a question about a record that is not on screen, and it is capped, so it
|
|
349
|
-
|
|
348
|
+
is not the whole story.
|
|
350
349
|
|
|
351
350
|
While a member has an app open, their chat agent can also **call that app's own
|
|
352
351
|
declared aliases** — named queries via `run_app_query`, workflows via
|
|
@@ -380,29 +379,42 @@ inputs object, and a select's legal values are your DECLARATION's — usually an
|
|
|
380
379
|
inline set that matches no table field — so an agent reading option values off
|
|
381
380
|
the underlying table would send ones your write rejects.
|
|
382
381
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
382
|
+
The app's own `description` heads that listing as `about:`, on both surfaces. It
|
|
383
|
+
is the only channel that reaches the agent on **every** turn without the member
|
|
384
|
+
saying anything, so a standing process belongs there: the recurring job the app
|
|
385
|
+
exists for, which alias does it, and what the agent must leave alone.
|
|
386
|
+
|
|
387
|
+
Three things follow for you as an author. **Your descriptions are what it
|
|
388
|
+
reads**, on this surface and inside your own agents' runs — they enter the
|
|
389
|
+
prompt as a capability listing, never as instructions. **A `select` input's
|
|
390
|
+
options are part of the contract the agent sees**, so prefer the
|
|
391
|
+
`field: "fld_…"` form when the values come from a real field: it resolves to
|
|
392
|
+
that field's CURRENT options at both render and validation time. And **an
|
|
393
|
+
input's `description` is the only place a DEFAULT is visible** — the catalog
|
|
394
|
+
renders a name, a type, a `?` and your prose, never the body. An optional input
|
|
395
|
+
whose default goes unstated is one the agent asks the member about instead of
|
|
396
|
+
omitting; write what omitting it means ("left blank = today").
|
|
397
|
+
|
|
398
|
+
**Put anything irreversible behind a
|
|
399
|
+
[`wait_for_approval`](./workflows.md#wait_for_approval--the-one-wait-worth-binding):**
|
|
400
|
+
an email, a shipment, a published post, a write over a value with no version to
|
|
401
|
+
restore. The body's own gate is the only human checkpoint on this path — it
|
|
402
|
+
names the act in the language of the business and reaches the approvers the
|
|
403
|
+
workflow declares.
|
|
404
|
+
|
|
405
|
+
**Shape a mutating alias around ONE call per job.** An alias that takes a single
|
|
406
|
+
record turns a fifteen-record job into fifteen executions and no single answer to
|
|
407
|
+
report, and a partial failure leaves it half done with nothing that says so. Take
|
|
408
|
+
an array input instead — one call, one outcome. A narrow write is right for a
|
|
409
|
+
screen's button, where the member picked the record by opening it; here nobody
|
|
410
|
+
picked anything.
|
|
411
|
+
|
|
412
|
+
**Report what it did NOT do.** The member's account of the write is assembled
|
|
413
|
+
from what the workflow returned and nothing else, so a batch that matched nine of
|
|
414
|
+
eleven and returns only success tells them all eleven were done. Return the
|
|
415
|
+
misses in `data`.
|
|
416
|
+
|
|
417
|
+
If a workflow should NOT be agent-reachable, it
|
|
406
418
|
does not belong in the manifest at all, since the member can already fire it
|
|
407
419
|
from the app's own buttons.
|
|
408
420
|
|
package/docs/data_fetching.md
CHANGED
|
@@ -170,8 +170,7 @@ const { rows, loadMore, hasMore, loadingMore } = useInfiniteQuery("feed", {}, {
|
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
The first render loads one page; `loadMore()` appends the next, accumulating into `rows`. Paging is
|
|
173
|
-
**keyset (seek)**
|
|
174
|
-
models above).
|
|
173
|
+
**keyset (seek)** (the pagination models above).
|
|
175
174
|
|
|
176
175
|
- `hasMore` is `true` while the last page came back **full** (a non-null cursor came back) — so a
|
|
177
176
|
total that is an exact multiple of `pageSize` costs one final short fetch before `hasMore` turns
|
|
@@ -398,13 +397,12 @@ pieces. Compose these — don't hand-roll search:
|
|
|
398
397
|
popover listbox with rich rows (`renderOptionContent`), keyboard navigation, `recentOptions`,
|
|
399
398
|
`allowCustom`. (For a known small list with no search box, `Picker`.)
|
|
400
399
|
- **A parameterized `search` query** — a `from_table` with `search: "{{params.q}}"` over the
|
|
401
|
-
maintained search document: **diacritics- and case-insensitive
|
|
402
|
-
|
|
403
|
-
`filter` (search within a scope). The full `search` contract is in
|
|
400
|
+
maintained search document: **diacritics- and case-insensitive**, trigram-indexed, and AND-ed
|
|
401
|
+
with the template's `filter` (search within a scope). The full `search` contract is in
|
|
404
402
|
[./queries.md](./queries.md). Reserve an OR-group of per-field `contains` only when you must
|
|
405
|
-
bound exactly which fields match — `contains` is
|
|
406
|
-
|
|
407
|
-
|
|
403
|
+
bound exactly which fields match — `contains` is **unindexed**, so a zero-match keystroke
|
|
404
|
+
forces a full table-partition scan. **Search-as-you-type uses `search`, never a `contains`
|
|
405
|
+
OR-group.**
|
|
408
406
|
- **`useQuery(alias, { q }, { enabled })`** — gate on a non-empty term: an empty term applies no
|
|
409
407
|
search constraint and would dump the table on first paint. `enabled` makes "nothing loads until
|
|
410
408
|
you type" true. Add `revalidateOnFocus: false` — re-running an ephemeral search on refocus is
|
package/docs/files.md
CHANGED
|
@@ -44,17 +44,16 @@ Signature: `dist/src/hooks.d.ts`. Returns `{ upload, uploading, error }`:
|
|
|
44
44
|
`{ id, filename, mime_type, url?, thumbnail_url? }` — `url`/`thumbnail_url` are presigned
|
|
45
45
|
(24 h) and load directly in the sandboxed iframe, so a just-uploaded image previews without a
|
|
46
46
|
round-trip.
|
|
47
|
-
- `fidelity` — **how faithful the stored image must be.**
|
|
48
|
-
|
|
49
|
-
and re-tunes them centrally as model tiers change.
|
|
47
|
+
- `fidelity` — **how faithful the stored image must be.** The platform owns the pixels behind
|
|
48
|
+
each step and re-tunes them centrally as model tiers change.
|
|
50
49
|
|
|
51
50
|
Defaults to `"high"` — enough to resolve the text of a photographed document. Drop to
|
|
52
51
|
`"standard"` when collecting in volume.
|
|
53
52
|
|
|
54
53
|
**This only affects photographs.** A PDF, Word, Excel or CSV file is stored untouched at every
|
|
55
|
-
step, so `"original"` is not what keeps a document intact — it already is.
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
step, so `"original"` is not what keeps a document intact — it already is. Note too that HEIC
|
|
55
|
+
(what an iPhone camera produces) is transcoded to JPEG server-side whatever you choose, because
|
|
56
|
+
no browser can render it.
|
|
58
57
|
|
|
59
58
|
| `fidelity` | Use it when | Stored at |
|
|
60
59
|
|---|---|---|
|
package/docs/mutations.md
CHANGED
|
@@ -302,10 +302,7 @@ await update_records({
|
|
|
302
302
|
```
|
|
303
303
|
|
|
304
304
|
`set` still writes every key it carries — `null` in `set` **clears** the field; `null`/absent in
|
|
305
|
-
`set_skip_null` **skips** it.
|
|
306
|
-
may appear in **at most one** of `set` / `set_skip_null` (naming it in both is rejected at run
|
|
307
|
-
time). An all-absent `set_skip_null` with no other write surface is a no-op — no records touched,
|
|
308
|
-
no `before_update` hooks. The full write surface (`set`, `set_skip_null`, the surgical
|
|
305
|
+
`set_skip_null` **skips** it. The full write surface (`set`, `set_skip_null`, the surgical
|
|
309
306
|
`add_to`/`remove_from`/`replace` ops, `field_edits`, and the exact value shape per field type)
|
|
310
307
|
is [workflows](./workflows.md#writing-records).
|
|
311
308
|
|
|
@@ -22,8 +22,7 @@ mechanism:
|
|
|
22
22
|
They compose: a screen is a path; the screen's filters ride as query params.
|
|
23
23
|
Neither is server state — `useUrlState` values are *client* state that you feed
|
|
24
24
|
into a query's *server* params (see [named-query params](./queries.md) and the
|
|
25
|
-
[data-fetching hooks](./data_fetching.md) they drive).
|
|
26
|
-
persisted server-side; the address bar is the only store.
|
|
25
|
+
[data-fetching hooks](./data_fetching.md) they drive).
|
|
27
26
|
|
|
28
27
|
The embedded-vs-standalone distinction below is the runtime's: embedded means
|
|
29
28
|
the app runs in an iframe inside the Lotics host, standalone means it runs on
|
package/docs/queries.md
CHANGED
|
@@ -6,8 +6,7 @@ Every read a custom-code app performs is a **named query**: a fixed AST template
|
|
|
6
6
|
for the hook-side contract). This document is the authoring reference for the query engine
|
|
7
7
|
itself: the node kinds, the source expressions, the per-field-type operator support, filters and
|
|
8
8
|
params, search, cross-table composition, server-side shaping, runtime refinement, and the
|
|
9
|
-
engine's hard limits.
|
|
10
|
-
a small closed surface, and knowing what composes (and what doesn't) up front saves hours.
|
|
9
|
+
engine's hard limits.
|
|
11
10
|
|
|
12
11
|
---
|
|
13
12
|
|
|
@@ -204,13 +203,12 @@ indexes live (§10). `sort` entries are `{ field_key, order: "asc"|"desc", blank
|
|
|
204
203
|
collection.** `{ "type": "files", "output": "photo", "source": "fld_…", "limit": 1 }` returns
|
|
205
204
|
the first entry per cell, in stored order, cut in SQL so the rest is never read or signed.
|
|
206
205
|
Without it the cell yields the whole array and the response pays to resolve every entry it
|
|
207
|
-
will never show
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
to bound — is rejected at deploy rather than ignored. **On a UNION, set it on every arm.**
|
|
206
|
+
will never show — at scale, into the response-wide presign ceiling (§10). The bound is per
|
|
207
|
+
CELL and caps at `FILES_PROJECTION_MAX_LIMIT` (10) — a row wanting more than a handful is
|
|
208
|
+
asking for the collection, which belongs to the record surface that opens it. It is a
|
|
209
|
+
different axis from the query's own `limit`, which counts ROWS; `limit` on any non-`files`
|
|
210
|
+
column — or on a computed source, which has no cell to bound — is rejected at deploy rather
|
|
211
|
+
than ignored. **On a UNION, set it on every arm.**
|
|
214
212
|
Arms align by column name and type, and `limit` is neither, so an arm that omits it still
|
|
215
213
|
yields whole cells — the query stays correct and silently costs what the bound was there to
|
|
216
214
|
avoid.
|
|
@@ -622,11 +620,10 @@ non-empty term client-side (`enabled`), or first paint dumps the table.
|
|
|
622
620
|
search document indexes select option labels, member names, dates in three formats, and each
|
|
623
621
|
linked record's cached display text — including the `Field=True/False` pill a boolean renders
|
|
624
622
|
into. A box captioned "find a person" then matches rows that merely share a stage label or an
|
|
625
|
-
owner, and folding makes near-homographs collide (`tiến` ≡ `tiền`).
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
when the box genuinely means *find this anywhere in the record*.
|
|
623
|
+
owner, and folding makes near-homographs collide (`tiến` ≡ `tiền`). When the box means *find
|
|
624
|
+
this person*, bound it: an OR-group of `contains` over the two or three identity fields (name,
|
|
625
|
+
national id, phone), with the term param `required: false` so an empty box prunes the group to
|
|
626
|
+
match-all. Use `search` when the box genuinely means *find this anywhere in the record*.
|
|
630
627
|
|
|
631
628
|
---
|
|
632
629
|
|
|
@@ -815,33 +812,18 @@ facet-aware, but it ships every matching row, so it holds only where the scope i
|
|
|
815
812
|
(one member's own records, a single project). A workspace-wide total that must follow a runtime
|
|
816
813
|
facet has no server-side shape today.
|
|
817
814
|
|
|
818
|
-
The SDK hooks map onto this directly
|
|
819
|
-
|
|
820
|
-
`useInfiniteQuery` pages by **keyset** (`keyset: true` + the prior `next_cursor`), so its scroll
|
|
821
|
-
never skips or duplicates a row; `usePaginatedQuery` owns the page cursor, pages by `offset`, and
|
|
822
|
-
issues a `count` keyed on `(alias, params, filter)` — independent of page and sort, so paging and
|
|
823
|
-
re-sorting never recount, while changing `(params, filter)` resets to page 0 and recounts. Given
|
|
824
|
-
its `total` option it issues none at all: a screen that already renders that figure from an
|
|
825
|
-
aggregate hands it over rather than buying it twice
|
|
826
|
-
([data_fetching](./data_fetching.md) → `usePaginatedQuery`).
|
|
815
|
+
The SDK hooks map onto this directly — the hook-side contract is
|
|
816
|
+
[data_fetching](./data_fetching.md).
|
|
827
817
|
|
|
828
818
|
Build `filter` from UI column-filters with `columnFilterToConditions` (`@lotics/ui`); prefer
|
|
829
819
|
`useFieldOptions` for a select filter's option set.
|
|
830
820
|
|
|
831
821
|
**Warning (transport gaps):** the embedded product host and the `lotics app dev` forwarder pass
|
|
832
822
|
`sort`/`filter`/`count` through. The **standalone public transport** (`<slug>.lotics.app`)
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
app
|
|
836
|
-
|
|
837
|
-
rely on runtime refinement.
|
|
838
|
-
|
|
839
|
-
**Pagination semantics.** `useQuery` / `usePaginatedQuery` and manual `rpc("query", { limit,
|
|
840
|
-
offset })` are **offset** — a deep page costs the server the full skipped prefix (page 400 of a
|
|
841
|
-
25-row pager scans ~10,000 rows before returning 25), and pages can shift under concurrent writes
|
|
842
|
-
(a row inserted before your offset repeats or skips a row across pages). `useInfiniteQuery` is
|
|
843
|
-
**keyset (seek)** — deep scrolls stay O(page) and never skip/duplicate rows. Keep offset browses
|
|
844
|
-
filtered and sorted by a stable key, and don't build numbered UX that walks thousands of pages.
|
|
823
|
+
forwards only `alias`/`params`/`limit`/`offset` — runtime `sort`/`filter` are silently ignored
|
|
824
|
+
there and a `count` never resolves ([data_fetching](./data_fetching.md) → Standalone transport).
|
|
825
|
+
A standalone app must bake ordering/scoping into the template (or params) rather than rely on
|
|
826
|
+
runtime refinement.
|
|
845
827
|
|
|
846
828
|
---
|
|
847
829
|
|
|
@@ -1062,8 +1044,6 @@ above, with `region` replaced by whatever the lookup pointed at.
|
|
|
1062
1044
|
|
|
1063
1045
|
## 12. Current limitations
|
|
1064
1046
|
|
|
1065
|
-
Consolidated from the sections above — these describe present engine behavior:
|
|
1066
|
-
|
|
1067
1047
|
- **No pivot/crosstab node.** Row-values-to-columns happens client-side over grouped results
|
|
1068
1048
|
(§8).
|
|
1069
1049
|
- **No `first_value`/`last_value`/`nth_value` navigation** — the `functions` set is `row_number`,
|
package/docs/recipes.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# Recipes — the app actions that are not obvious
|
|
2
2
|
|
|
3
3
|
The other area docs describe contracts. This one is task-shaped: **"how do I do X"**, for the
|
|
4
|
-
handful of actions whose mechanism is not guessable from the hooks alone.
|
|
5
|
-
against the running platform once — the point of writing them down is that nobody re-derives them.
|
|
4
|
+
handful of actions whose mechanism is not guessable from the hooks alone.
|
|
6
5
|
|
|
7
6
|
---
|
|
8
7
|
|
|
@@ -11,7 +10,7 @@ against the running platform once — the point of writing them down is that nob
|
|
|
11
10
|
A button that produces a file — a quotation, a debit note, a label — and hands it over, touching
|
|
12
11
|
**no** record.
|
|
13
12
|
|
|
14
|
-
**The mechanism is the return channel
|
|
13
|
+
**The mechanism is the return channel.** Any workflow tool
|
|
15
14
|
whose step output carries a `file_id` (`generate_pdf_from_template`, `generate_excel_from_template`,
|
|
16
15
|
`generate_word_from_template`) is auto-collected by the execute endpoint and comes back in
|
|
17
16
|
**`result.files[]`**, each with a servable `url`.
|
package/docs/runtime.md
CHANGED
|
@@ -134,9 +134,7 @@ clear the gate: `<slug>.lotics.app` is served by the app-host Worker, which
|
|
|
134
134
|
withholds every byte of the bundle and serves a password page instead. On
|
|
135
135
|
success it sets `lotics_app_session` on the app's origin; the SDK reads that
|
|
136
136
|
cookie and forwards it as `X-Lotics-App-Session` on data calls. The SDK owns no
|
|
137
|
-
password UI
|
|
138
|
-
so it lives at the serving layer where one styled, localized page covers every
|
|
139
|
-
public app. A `401` with error code `PASSWORD_REQUIRED` (the owner rotated or
|
|
137
|
+
password UI. A `401` with error code `PASSWORD_REQUIRED` (the owner rotated or
|
|
140
138
|
cleared the password) reloads the page, which puts the visitor back through the
|
|
141
139
|
gate. What the password does and doesn't protect: [security](./security.md).
|
|
142
140
|
|
|
@@ -173,9 +171,7 @@ surfaces:
|
|
|
173
171
|
| `agentRuns` (session history) | **no** — the product host doesn't implement it either (`"Unknown RPC op: agentRuns"`) | **no** | yes |
|
|
174
172
|
| `askAi` | yes | **no** — `"Unknown RPC op: askAi"` | rejects — `"askAi is only available when the app runs inside Lotics"` |
|
|
175
173
|
|
|
176
|
-
**Limitation:**
|
|
177
|
-
embedded product host *or* the dev forwarder — both error `"Unknown RPC op:
|
|
178
|
-
agentRuns"`. Don't build a session-log UI on `useAgentRuns`; keep the log in app
|
|
174
|
+
**Limitation:** Don't build a session-log UI on `useAgentRuns`; keep the log in app
|
|
179
175
|
state from live `useAgentRun` results (see [ai](./ai.md)). Server-side *cancel*
|
|
180
176
|
also errors in the dev loop (the local abort of a live stream still works) —
|
|
181
177
|
verify cancel on a deployed app. The standalone `query` transport forwards only
|
|
@@ -355,8 +351,7 @@ install `posthog-js` or call any analytics API from app code.
|
|
|
355
351
|
replay are off (autocapture is disabled at the project level — a client flag
|
|
356
352
|
could not re-enable it). System signals are deliberately not events: data
|
|
357
353
|
reads (`useQuery` fetches) and workflow/agent run outcomes. The backend
|
|
358
|
-
already logs and persists every run (`workflow_executions`, `app_agent_runs`)
|
|
359
|
-
so a client run event would be redundant system telemetry, not a gesture.
|
|
354
|
+
already logs and persists every run (`workflow_executions`, `app_agent_runs`).
|
|
360
355
|
- Events the SDK emits automatically:
|
|
361
356
|
|
|
362
357
|
| Event | Fired when | Properties |
|
package/docs/workflows.md
CHANGED
|
@@ -540,6 +540,30 @@ the **diff**: `changes["fld_link"]?.next_value` / `?.prev_value` are plain id ar
|
|
|
540
540
|
has nowhere to attach a fetch, so it cannot descend either), and they feed `set:` directly.
|
|
541
541
|
Everywhere else, write `[x.id]`.
|
|
542
542
|
|
|
543
|
+
### A write that will be re-run
|
|
544
|
+
|
|
545
|
+
The tables above answer *what the caller sent*. A workflow an agent can reach
|
|
546
|
+
needs the other axis too — **what the record already holds** — because the same
|
|
547
|
+
call arrives twice: a recurring report a week later, a retry, one batch
|
|
548
|
+
overlapping the last. On that second pass an unconditional `set` reports success
|
|
549
|
+
and silently replaces what was there.
|
|
550
|
+
|
|
551
|
+
Decide per field, and expect the two directions of one operation to be
|
|
552
|
+
**asymmetric**. A field recording *when* something happened is written only while
|
|
553
|
+
it is still empty, while its undo clears unconditionally:
|
|
554
|
+
|
|
555
|
+
```js
|
|
556
|
+
if (i.undo) {
|
|
557
|
+
await update_records({ table_id: "tbl_x", record_ids: [row.id], set: { fld_submitted_on: null } });
|
|
558
|
+
} else if (isNull(row["fld_submitted_on"])) {
|
|
559
|
+
await update_records({ table_id: "tbl_x", record_ids: [row.id], set: { fld_submitted_on: i.on } });
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
There is no version to restore a cell from, so prove it before anyone can call
|
|
564
|
+
it: `lotics app workflow run <alias>` the same input twice, and confirm the
|
|
565
|
+
second run changed nothing.
|
|
566
|
+
|
|
543
567
|
### Authorizing the caller
|
|
544
568
|
|
|
545
569
|
A workflow runs under the **app owner's** authority, so its own principal tells you nothing about
|