@lotics/cli 0.302.0 → 0.303.1

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.
@@ -55,7 +55,7 @@ Every node has a `kind`; all but `from_table` read the node under `from` (`join`
55
55
  | `filter` | `predicate` — a filter tree over the input's columns |
56
56
  | `join` | `left`, `right`, `on: { left_column, right_column }`, `type: "inner" \| "left"` |
57
57
  | `union` | `sources` (at least two, columns aligned by name and type) |
58
- | `group` | `by` (columns, or `{ bucket: { source, granularity, output } }`), `aggregates` |
58
+ | `group` | `by` (columns, `{ bucket: { source, granularity, output } }`, or `{ band: { source, tiers, output } }`), `aggregates` |
59
59
  | `window` | `partition_by`, `order_by`, `frame?`, `aggregates?`, `functions?` |
60
60
  | `sort` | `by: [{ field_key, order }]` |
61
61
  | `limit` | `n`, `offset?` or `keyset?` |
@@ -124,7 +124,12 @@ keeps the type tracking the live field.
124
124
  ## Workflows
125
125
 
126
126
  `set_app_workflow` binds a workflow body to an alias: `{ app_id, alias, source, inputs?, outputs?,
127
- name?, description? }`.
127
+ name?, description? }`. On an app built from a model, the alias is one the model names — an act's own
128
+ `workflow`, or an entry of the app's `workflows`, which states `inputs`, `outputs` and `description`:
129
+ a call stating any of them for it is refused, and outputs the entry leaves out are derived from the
130
+ body. An alias the model names nowhere is archived by the next apply. An entry of `workflows` runs
131
+ only for someone who holds the app: on an app shared by a public link, a visitor's call of it is
132
+ refused as an alias the app does not have.
128
133
 
129
134
  - `source` is the body with no `on({...})` trigger — the app is the trigger. It reads each declared
130
135
  input as `trigger.app_workflow.inputs.<name>`; no record is in scope, so it reads each one by id
@@ -102,7 +102,11 @@ decisions beside the workspace's rows, and with `--json` gives each app as the p
102
102
 
103
103
  **The model changes after the app exists, and applying it again is how it lands.** Edit the file,
104
104
  apply it. An act whose write the model cannot say names its own `workflow`; that workflow's body is
105
- the live one, and `lotics run set_app_workflow` changes it. An act that reads papers runs an agent
105
+ the live one, and `lotics run set_app_workflow` changes it. A job no press starts — an agent or a
106
+ service handing the app a file or a list of rows — is a workflow the app declares in `workflows`
107
+ (`lotics docs model/apps`): the model states what it takes and hands back, `lotics run
108
+ set_app_workflow` sets its body with `inputs` and `outputs` left out, and every apply keeps that
109
+ body. A workflow set on the app that its model declares nowhere is archived by the next apply. An act that reads papers runs an agent
106
110
  apply owns: made, replaced and removed with the act, and an agent of that alias apply did not make
107
111
  is refused, never rewritten.
108
112
 
@@ -181,7 +185,9 @@ model finds of that app now, beside its rows — what the next apply would refus
181
185
  on it, the `patch` adopting the decisions among them, and the app's body as the patch
182
186
  leaves it (`draft`). `verdict_unread` says why none was read.
183
187
 
184
- Then set the icon, the colour and the app's own `description` through `lotics run update_app`. The
188
+ The app's `name`, `icon`, `theme` and `description` are keys of the app in the model, and each apply
189
+ writes them onto the app; one the model leaves out is cleared. `lotics run update_app` changes them on
190
+ the app, and `lotics model pull` reads them back as the app holds them. The
185
191
  `description` heads the capability listing the member's chat agent reads on **every** turn, so a
186
192
  standing process the app expects that agent to carry out belongs there and nowhere else.
187
193
 
@@ -980,6 +980,37 @@ above. A provider's refusal carries the provider's own words.
980
980
  | `truncated_cells[].cell` | string | yes | Cell address |
981
981
  | `truncated_cells[].original_length` | number | yes | Original text length |
982
982
 
983
+ ### excel_get_rows (workflow step)
984
+
985
+ - Read every row of a sheet in an .xlsx, .xls or .csv file as a list of objects keyed by the header row's text.
986
+ - A cell is its typed value: a number, true or false, text, a date as ISO text ("2026-03-02", "2026-03-02T09:30:00", or "09:30:00" for a time alone), null when blank.
987
+ - A formula cell is the value the file last computed; an error cell is its code as text ("#N/A").
988
+ - Every CSV cell is text or null.
989
+ - A header cell inside a merge takes the merge's text; a blank header is keyed by its column letter ("C"), and a header text that repeats gets its column letter added ("Amount (F)"). headers lists the keys in sheet order.
990
+ - Rows above header_row and rows blank in every column are left out.
991
+ - One call reads a sheet of at most 10000 rows, its header counted, and returns at most 250000 cells (rows × kept columns); a sheet past either is refused with its row count, never cut short.
992
+ - Read-only.
993
+
994
+ **Inputs**
995
+
996
+ | Name | Type | Required | What it is |
997
+ | --- | --- | --- | --- |
998
+ | `file_id` | string | yes | |
999
+ | `worksheet` | string \\| number | | Sheet name, or 0-based index. |
1000
+ | `header_row` | number | | 1-based row holding the headers. |
1001
+ | `columns` | string[] | | Keys to keep, as headers lists them. |
1002
+
1003
+ **Answer**
1004
+
1005
+ | Name | Type | Required | What it is |
1006
+ | --- | --- | --- | --- |
1007
+ | `file_id` | string | yes | |
1008
+ | `worksheet_name` | string | yes | |
1009
+ | `header_row` | number | yes | |
1010
+ | `headers` | string[] | yes | The key of each returned column, in sheet order |
1011
+ | `row_count` | number | yes | |
1012
+ | `rows` | object[] | yes | |
1013
+
983
1014
  ### excel_update_range (workflow step)
984
1015
 
985
1016
  - Write cell values into a range in an Excel worksheet. range format is always "START:END" — single cell: "B2:B2", multi-cell: "A1:C3".
package/docs/workflows.md CHANGED
@@ -24,11 +24,6 @@ on({ type: "recurring_schedule", cron_expression: "0 9 * * MON" });
24
24
  Every source below is typed, so a misspelled member is refused at save rather than read as nothing.
25
25
  All three carry `trigger.trigger_id`.
26
26
 
27
- An automation that brings rows in — on a schedule, or a webhook an outside system calls — is marked
28
- as importing into their table with `imports_into` (a table id) when it is created or edited. Its
29
- runs are then the imports log of a register over that table that keeps one (`register.imports`);
30
- on the CLI, `get_automation` and `query_automations` read the mark back.
31
-
32
27
  | trigger | required config | what the body reads |
33
28
  |---|---|---|
34
29
  | `recurring_schedule` | `cron_expression` | `runtime.timezone`; the current time is `now()` |
@@ -288,6 +283,46 @@ if (customer && includes(record["fld_customer"], customer.id)) {
288
283
  }
289
284
  ```
290
285
 
286
+ Read an uploaded sheet and add the rows a table lacks. `excel_get_rows`, a workflow step, answers every
287
+ row of an `.xlsx`, `.xls` or `.csv` sheet in one call, each an object keyed by the header row's text. A cell is a number,
288
+ `true` or `false`, text, a date as ISO text, or null when blank, so `row["Header"]` is typed
289
+ `string | number | boolean | null` and a helper narrows it: `toString` for text, `toNumber` for a number,
290
+ `isString` before a date. Every cell of a `.csv` is text. `columns` keeps only the ones the body reads.
291
+ A sheet past what one call reads is refused with its size, never cut short: the step's entry in
292
+ `workflow_steps` states the bounds.
293
+
294
+ A table can hold more rows than one page of `query_records` answers, so the body never reads "the
295
+ table" to see what it lacks. It takes the sheet fifty rows at a time — what one `create_records` call
296
+ writes — asks the table for those rows' codes alone, and writes the rest in one call:
297
+
298
+ ```
299
+ const sheet = await excel_get_rows({ // a workflow step
300
+ file_id: trigger.app_workflow.inputs.file,
301
+ columns: ["Code", "Filed", "Amount"],
302
+ });
303
+
304
+ for (let start = 0; start < sheet.row_count; start += 50) {
305
+ const rows = uniqueBy(slice(sheet.rows, start, start + 50), (row) => toString(row["Code"]));
306
+ const held = await query_records({
307
+ table_id: "tbl_declarations",
308
+ filters: { field_key: "fld_code", operator: "is_any_of", value: rows.map((row) => toString(row["Code"])) },
309
+ pagination: { offset: 0, limit: 1000 },
310
+ });
311
+ const known = held.records.map((r) => toString(r.data["fld_code"]));
312
+ const fresh = filter(rows, (row) => !includes(known, toString(row["Code"])));
313
+ if (size(fresh) > 0) {
314
+ await create_records({
315
+ table_id: "tbl_declarations",
316
+ records: fresh.map((row) => ({
317
+ fld_code: toString(row["Code"]),
318
+ fld_filed: isString(row["Filed"]) ? row["Filed"] : null,
319
+ fld_amount: toNumber(row["Amount"]),
320
+ })),
321
+ });
322
+ }
323
+ }
324
+ ```
325
+
291
326
  ## Table lifecycle workflows
292
327
 
293
328
  A lifecycle workflow is bound to one table and one event, and its source has no `on({...})` line:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.302.0",
3
+ "version": "0.303.1",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {