pgkiln 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/SECURITY.md +7 -0
  2. package/db/migrations/119_workspace_home.sql +30 -0
  3. package/db/migrations/120_datasets.sql +87 -0
  4. package/db/migrations/121_ai_agents.sql +94 -0
  5. package/db/migrations/122_team_development.sql +186 -0
  6. package/db/migrations/123_workflow_outcomes.sql +407 -0
  7. package/db/migrations/124_component_details.sql +51 -0
  8. package/docs/apex-feature-parity.md +15 -12
  9. package/docs/guide/01-installation.md +9 -3
  10. package/docs/guide/03-builder.md +211 -11
  11. package/docs/guide/04-pages-and-regions.md +96 -22
  12. package/docs/guide/05-items.md +7 -1
  13. package/docs/guide/06-processing.md +123 -8
  14. package/docs/guide/07-dynamic-actions.md +34 -0
  15. package/docs/guide/09-reference.md +2 -1
  16. package/docs/guide/11-from-apex.md +2 -1
  17. package/docs/guide/12-development.md +70 -11
  18. package/docs/guide/20-ai-agents.md +4 -2
  19. package/examples/hr/README.md +2 -0
  20. package/examples/hr/hr_63_datasets.sql +29 -0
  21. package/examples/hr/hr_64_ai_agent.sql +97 -0
  22. package/examples/hr/hr_65_ask_reports.sql +21 -0
  23. package/examples/hr/hr_66_team_development.sql +58 -0
  24. package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
  25. package/examples/hr/hr_68_component_details.sql +96 -0
  26. package/package.json +1 -1
  27. package/public/app.css +46 -0
  28. package/public/app.js +134 -20
  29. package/public/builder.css +116 -0
  30. package/public/builder.js +47 -0
  31. package/src/ai/chat.ts +18 -18
  32. package/src/ai/service.ts +35 -11
  33. package/src/ai/types.ts +7 -2
  34. package/src/app.ts +11 -1
  35. package/src/builder/advisor.ts +6 -3
  36. package/src/builder/ai-agents.ts +372 -0
  37. package/src/builder/ai-builder.ts +3 -3
  38. package/src/builder/ai.ts +13 -9
  39. package/src/builder/assistant.ts +22 -7
  40. package/src/builder/components.ts +38 -9
  41. package/src/builder/datasets.ts +472 -0
  42. package/src/builder/designer.ts +8 -3
  43. package/src/builder/feedback.ts +6 -1
  44. package/src/builder/home.ts +12 -7
  45. package/src/builder/instance.ts +2 -0
  46. package/src/builder/locks.ts +7 -1
  47. package/src/builder/region-help.ts +2 -2
  48. package/src/builder/reporter.ts +52 -3
  49. package/src/builder/routes.ts +7 -4
  50. package/src/builder/shared.ts +1 -1
  51. package/src/builder/subscriptions.ts +1 -0
  52. package/src/builder/team.ts +991 -0
  53. package/src/builder/ui.ts +8 -5
  54. package/src/builder/workflow-migrate.ts +210 -0
  55. package/src/builder/workflows.ts +29 -6
  56. package/src/builder/workspace-home.ts +285 -0
  57. package/src/builder/workspace-transfer.ts +1 -1
  58. package/src/builder/workspaces.ts +7 -4
  59. package/src/cli/replace.ts +7 -1
  60. package/src/datasets.ts +367 -0
  61. package/src/i18n/ar.ts +33 -0
  62. package/src/i18n/cs.ts +33 -0
  63. package/src/i18n/da.ts +33 -0
  64. package/src/i18n/de.ts +33 -0
  65. package/src/i18n/el.ts +33 -0
  66. package/src/i18n/es.ts +33 -0
  67. package/src/i18n/fi.ts +33 -0
  68. package/src/i18n/fr.ts +33 -0
  69. package/src/i18n/he.ts +33 -0
  70. package/src/i18n/it.ts +33 -0
  71. package/src/i18n/ja.ts +33 -0
  72. package/src/i18n/ko.ts +33 -0
  73. package/src/i18n/nb.ts +33 -0
  74. package/src/i18n/pl.ts +33 -0
  75. package/src/i18n/pt.ts +33 -0
  76. package/src/i18n/ru.ts +33 -0
  77. package/src/i18n/sv.ts +33 -0
  78. package/src/i18n/tr.ts +33 -0
  79. package/src/i18n/uk.ts +33 -0
  80. package/src/i18n/zh.ts +33 -0
  81. package/src/i18n.ts +66 -0
  82. package/src/instance.ts +3 -0
  83. package/src/metadata.ts +9 -3
  84. package/src/release-check.ts +139 -0
  85. package/src/reporting-app.ts +101 -0
  86. package/src/runtime/ai-agents.ts +365 -0
  87. package/src/runtime/ai-filter.ts +385 -107
  88. package/src/runtime/assistant.ts +64 -11
  89. package/src/runtime/context.ts +2 -0
  90. package/src/runtime/data-reporter.ts +199 -32
  91. package/src/runtime/ir-phrases.ts +812 -0
  92. package/src/runtime/ir-plan.ts +449 -0
  93. package/src/runtime/items.ts +1 -1
  94. package/src/runtime/regions.ts +6 -2
  95. package/src/runtime/render.ts +1 -1
  96. package/src/runtime/report.ts +1 -1
  97. package/src/runtime/tasks.ts +63 -12
  98. package/src/session.ts +2 -0
  99. package/src/subscriptions.ts +2 -1
  100. package/src/task-events.ts +232 -0
  101. package/src/team-samples.ts +74 -0
  102. package/src/team.ts +342 -0
  103. package/src/workflow.ts +135 -18
  104. package/src/workspace-export.ts +39 -4
@@ -256,7 +256,7 @@ Features for end users:
256
256
  computed columns and views under a name, and switch between saved reports (below);
257
257
  - **rows per page**, **download CSV / Excel / PDF**, **print**, **reset**.
258
258
  - Active search, filters, break, aggregates, highlights, computed columns and views appear as
259
- removable chips. Once a group by, pivot or chart is set, **Report / Group by / Pivot / Chart**
259
+ removable chips (with *Ask in your own words*, as chips with Edit and Remove under the question box). Once a group by, pivot or chart is set, **Report / Group by / Pivot / Chart**
260
260
  links switch between the views; all views use the same search, filters and facets.
261
261
  - **Paging** with Previous/Next.
262
262
  - On phones every row **reflows** into a card with labelled values.
@@ -267,26 +267,71 @@ and highlight values become literals, columns must exist in the result, operator
267
267
  functions, chart types and colors come from fixed lists, sort positions are integers, and computed
268
268
  column expressions are parsed (below), never pasted into SQL.
269
269
 
270
- ### Natural-language filters (*Ask in your own words*)
271
-
272
- With an [AI service](06-processing.md#generate-text-with-ai), an interactive report can take a
273
- question in the user's own words (APEX: natural language to interactive report): *"analysts and
274
- managers hired before 1982, highest salary first"*. Set `"ai_filter": {"service": "NAME"}` in the
275
- report's attributes (or *Ask in your own words (AI)* under the report's settings in the page
276
- designer); a question box appears above the report when the application may use the service.
277
-
278
- The question goes to the service with the report's shown columns (names, headings and kinds:
279
- text, number, date) and its filter operators; the answer is a structured output (filters, a search
280
- text, a sort column) that pgkiln checks again (only those columns and operators, at most five
281
- filters, short values) and turns into the report's ordinary URL parameters. So the model can only
282
- set what the user could set by hand in the Actions menu; the report builds the SQL (values as
283
- escaped literals) and runs it as the application's role, and the user sees which filters were
284
- applied and can change or remove them. Hidden columns (`hidden`) are not offered. Signed-in users
285
- only, unless `"public": true`; logged in the AI usage log with the source `nl2ir`.
270
+ ### Ask in your own words (AI interactive reports)
271
+
272
+ An interactive report can take a request in the user's own words (APEX 26.1/26.2: AI interactive
273
+ reports): *"sum of salary by department where job is clerk"*, *"managers hired before 1982, highest
274
+ salary first"*, *"bonus is 10% of salary"*. Set `"ai_filter": {}` in the report's attributes (or
275
+ *Ask in your own words* under the report's settings in the page designer); a question box appears
276
+ above the report.
277
+
278
+ The answer is never SQL: it becomes the report's **own settings**, the same ones the Actions menu
279
+ sets: filters, highlights, a search, a sort, a control break, totals (aggregates), a group by, a
280
+ pivot, a chart and computed columns. They are listed under the box as **chips**; each chip has
281
+ **Edit** (a small form with its column, operator, value, function, color or expression) and
282
+ **Remove**, and **Undo** returns to the settings before the last change (the last five steps, kept
283
+ in the session). A new request refines the current settings: a filter on a column replaces that
284
+ column's filters, a sort, break, group by, pivot or chart replaces the old one, highlights, totals
285
+ and computed columns are added; *"start over"* clears them. The chips are buttons of one form, so
286
+ they work without JavaScript.
287
+
288
+ **Without an AI service** pgkiln reads the request itself with a rule-based parser for common
289
+ phrases (APEX 26.2: works with zero AI). Columns are recognised by name or heading (also without
290
+ spaces, and verbs such as *hired* for `hiredate`); values such as *"only clerks"* or *"in sales"*
291
+ are looked up among the report's own rows (as a query parameter); what isn't understood is said
292
+ in the message, not guessed. Phrases it understands, in English (always), Dutch and German (when
293
+ that is the user's language):
294
+
295
+ | Request | Becomes |
296
+ |---|---|
297
+ | *salary over 3000*, *salary between 1,000 and 3k*, *hired before june 1981*, *hired in 1981*, *name contains "mi"*, *no commission* | filters (`gt`, `ge` + `le`, `lt`, a date range, `contains`, `null`) |
298
+ | *only clerks*, *not clerks*, *in sales*, *active* | a filter on the column that holds the value |
299
+ | *sort by hire date, newest first*, *highest salary first* | the sort |
300
+ | *sum of salary by department*, *count by department and job* | a group by with its function |
301
+ | *total salary*, *average salary* | a totals row |
302
+ | *break by department* | a control break |
303
+ | *pivot sum of salary by department and job* | a pivot |
304
+ | *pie chart of count by department*, *chart of salary by job* | a chart |
305
+ | *highlight salary over 4000 in red* | a highlight |
306
+ | *bonus is 10% of salary*, *yearly = sal * 12* | a computed column |
307
+ | *search for smith* | the search |
308
+ | *start over*, *clear all* | no settings |
309
+
310
+ Dutch (*salaris boven 3000*, *sorteer op datum aflopend*, *som van salaris per afdeling*, *alleen
311
+ klerken*, *bonus is 10% van salaris*) and German (*Gehalt über 3000*, *sortiere nach Datum
312
+ absteigend*, *Summe von Gehalt pro Abteilung*, *nur Angestellte*) work the same way; another
313
+ language adds a phrase table to `src/runtime/ir-phrases.ts`.
314
+
315
+ **With an AI service** (`"ai_filter": {"service": "NAME"}`, an [AI
316
+ service](06-processing.md#generate-text-with-ai) the application may use), the request goes to the
317
+ service with the report's shown columns (names, headings and kinds: text, number, date, boolean),
318
+ its operators and functions and the current settings; the answer is a structured output. When the
319
+ service is disabled or not allowed for the application, the built-in phrases answer instead. The
320
+ request and the column names go to the service's provider; logged in the AI usage log with the
321
+ source `nl2ir`.
322
+
323
+ Either way the answer is checked again before it is applied: only the report's shown columns
324
+ (`hidden` columns are neither offered nor accepted), operators, functions, chart types and colors
325
+ from the report's fixed lists, at most five filters, highlights and totals, short values (which
326
+ become escaped literals or query parameters), and computed expressions that pass the same
327
+ validator as Actions → Compute (below). A chip edit is checked the same way; a form that names a
328
+ hidden column, an unknown operator or a chip that isn't there is refused. The report builds the
329
+ SQL and runs it as the application's role. Signed-in users only, unless `"public": true`.
286
330
 
287
331
  | Key | Meaning |
288
332
  |---|---|
289
- | `ai_filter.service` | The AI service's name |
333
+ | `ai_filter` | `{}` for the built-in phrases |
334
+ | `ai_filter.service` | An AI service's name (the built-in phrases when it isn't available) |
290
335
  | `ai_filter.placeholder` | The question box's placeholder |
291
336
  | `ai_filter.public` | Also for users who are not signed in |
292
337
 
@@ -1186,9 +1231,12 @@ Attributes (written by the region's **Settings**):
1186
1231
 
1187
1232
  | Key | Default | Meaning |
1188
1233
  |---|---|---|
1189
- | `sources` | | `[{"id": "orders", "label": "Orders", "description": "…", "schema": "sales", "table": "order_v", "columns": [{"name": "total", "label": "Total", "format": "FML999G990D00"}]}]` |
1234
+ | `sources` | | `[{"id": "orders", "label": "Orders", "description": "…", "schema": "sales", "table": "order_v", "columns": [{"name": "total", "label": "Total", "format": "FML999G990D00"}]}]`; (sprint 110) or a dataset: `{"id": "sales", "dataset": "sales"}` (label and description optional) |
1190
1235
  | `sharing` | `true` | `false`: reports stay private |
1191
1236
  | `share_authz` | | An authorization scheme: only users that pass it may share reports |
1237
+ | `edit_authz` | | (sprint 110) An authorization scheme: only users that pass it build, change, save and delete reports; the others only run shared ones |
1238
+ | `admin_authz` | | (sprint 110) An authorization scheme: users that pass it unpublish and delete anyone's reports |
1239
+ | `publishing` | `false` | (sprint 110) `true`: shared reports are *published*: a stable URL, Publish / Unpublish buttons, pins |
1192
1240
  | `page_size` | `25` | Rows per page of a detail report (5 to 200) |
1193
1241
  | `welcome` | `true` | `false`: the compact start (a list of sources and *Start*) instead of the welcome page |
1194
1242
  | `welcome_title`, `welcome_text` | | The welcome page's greeting instead of pgkiln's (translated like other application texts) |
@@ -1200,6 +1248,27 @@ region when an application is replaced. The HR example's page 36 (*My reports*)
1200
1248
  (a view without the user name and photo columns) and leave requests (row level security: an
1201
1249
  employee sees only their own), with King's shared report *Salary by department*.
1202
1250
 
1251
+ #### Datasets and reporting applications
1252
+
1253
+ (sprint 110) A source may be a **dataset** of the workspace ([App Builder → Data
1254
+ Reporter](03-builder.md#data-reporter-datasets-and-reporting-applications)): tables and views of one schema with
1255
+ their joins, the offered columns with labels, format masks and descriptions (shown under the column in the editor),
1256
+ and a row filter. The region's Settings add one under *Add a data source → Or a dataset of the workspace*. The
1257
+ runtime reads the dataset when the page is drawn and builds its SELECT from the definition (identifiers quoted,
1258
+ values escaped literals, operators from the whitelist), and the report runs on it as a subquery, as the
1259
+ application's database role. A dataset that was deleted disappears from the region; a column that no longer exists
1260
+ makes the report show an error.
1261
+
1262
+ With **`publishing`**, shared reports are *published*: an editor saves a report privately and **Publishes** it
1263
+ (or ticks *Publish for all users* when saving), and **Unpublishes** it again. A published report has a **stable
1264
+ URL**, `/a/<alias>/reports/<id>`, shown on the report: it opens the report on its page (signing in first), so it
1265
+ can be bookmarked or sent. Every signed-in user may **Pin** a published report: pinned reports come first on the
1266
+ welcome page (a [preference](06-processing.md) of the user, `reporter.pins.<region>`). Users that don't pass
1267
+ `edit_authz` see only the published reports: no *New report* cards, no editor (report parameters in the URL are
1268
+ ignored), no saving. Users that pass `admin_authz` unpublish and delete anyone's reports. The HR example's dataset
1269
+ `hr` (`examples/hr/hr_63_datasets.sql`: employees with their department, active ones only) is the one a reporting
1270
+ application is made from in the guide's chapter 3.
1271
+
1203
1272
  ### `form_designer` (Form Designer)
1204
1273
 
1205
1274
  A **Form Designer** region (sprint 85; pgkiln's own, APEX has no counterpart) lets the application's
@@ -1273,8 +1342,12 @@ forms *Departments* and *Book a meeting*.
1273
1342
  ### `ai_assistant` (AI assistant)
1274
1343
 
1275
1344
  An **AI assistant** region (APEX: AI Assistant, AI agents and tools) is a chat with an
1276
- [AI service](06-processing.md#generate-text-with-ai) (Claude or OpenAI) that an administrator
1277
- configured and allowed for the application. The developer gives it a system prompt and, optionally,
1345
+ [AI service](06-processing.md#generate-text-with-ai) (Claude, OpenAI, Gemini or an OpenAI-compatible
1346
+ endpoint) that an administrator configured and allowed for the application. Either the region names an
1347
+ **[AI agent](03-builder.md#ai-agents)** of the application (`"agent": "SALES_AGENT"`: the agent sets the
1348
+ service, system prompt, reasoning effort, tools, steps and output limit, and its authorization scheme
1349
+ decides who may use the region; tools may also be PL/pgSQL functions, JavaScript actions and links to
1350
+ pages), or the developer gives the region a system prompt and, optionally,
1278
1351
  **context queries** and **tools**; the model then answers questions about the application's data,
1279
1352
  calling the tools when it needs to, like an agent. It works without JavaScript (the message is
1280
1353
  posted and the page comes back with the answer); with JavaScript the Send button shows that the
@@ -1320,6 +1393,7 @@ Attributes (written by the region's **Settings**; they travel with the applicati
1320
1393
 
1321
1394
  | Key | Default | Meaning |
1322
1395
  |---|---|---|
1396
+ | `agent` | | (121) An [AI agent](03-builder.md#ai-agents) of the application; then leave out `service`, `system`, `tools` and `max_rounds` |
1323
1397
  | `service` | | The AI service's name |
1324
1398
  | `system` | | The system prompt (`&ITEM.` substitutions as delimited data) |
1325
1399
  | `welcome` | | The first message shown (translatable) |
@@ -1333,7 +1407,7 @@ Attributes (written by the region's **Settings**; they travel with the applicati
1333
1407
 
1334
1408
  The HR example's page 38 (*HR assistant*) has an assistant with a context query over `hr.policy`
1335
1409
  and four tools (the user's own leave, colleagues, departments, and *request_leave*, which files a
1336
- leave request), next to a staff list with [*Ask in your own words*](#natural-language-filters-ask-in-your-own-words).
1410
+ leave request), next to a staff list with [*Ask in your own words*](#ask-in-your-own-words-ai-interactive-reports).
1337
1411
 
1338
1412
  ### Template components
1339
1413
 
@@ -217,6 +217,11 @@ item's config. A combobox is free text and isn't checked.
217
217
  A `popup_lov` item shows the chosen value's display text and a **Search** button. The button
218
218
  opens a dialog with a search field: the server searches the item's own list of values (case
219
219
  insensitive, on the display column and any extra columns) and answers one page at a time.
220
+ **The next page loads as the list scrolls** (APEX: infinite scrolling): when the end of the list
221
+ comes into view the dialog fetches the next `page_size` rows and adds them, until the list of
222
+ values has no more, so a list of thousands of rows scrolls to its end without a *More* button. A
223
+ new search starts over at the first page. A browser without `IntersectionObserver` pages with
224
+ ‹ and *More* instead.
220
225
 
221
226
  - **Several columns**: the first column is the display value, the second the return value, and
222
227
  any further columns are shown in the dialog (and searched), for example:
@@ -233,7 +238,8 @@ insensitive, on the display column and any extra columns) and answers one page a
233
238
  parameter (never SQL text), only for an item the user may see and change on a page they may
234
239
  open. A submitted value must be one the list of values returns, or the page shows an error.
235
240
 
236
- The HR example's page 26 (*Pick an employee*) has one with name, job and department columns.
241
+ The HR example's page 26 (*Pick an employee*) has one with name, job and department columns;
242
+ page 47 (*Component details*) one over 6,000 rows, 100 a page.
237
243
 
238
244
  ## Cascading lists of values
239
245
 
@@ -28,13 +28,14 @@ simply closes the dialog.
28
28
  ### Menu buttons and badges
29
29
 
30
30
  A button with action `menu` opens a dropdown (an HTML `<details>` element, so it works without
31
- JavaScript). `menu` is a JSON array of at most 20 entries; each is either a **link** to a page of
32
- the application or a **submit request**:
31
+ JavaScript). `menu` is a JSON array of at most 20 entries; each is a **link** to a page of
32
+ the application, a **submit request** or an **action** that runs dynamic actions:
33
33
 
34
34
  ```json
35
35
  [{"label": "Leave requests", "page": 6, "icon": "list"},
36
36
  {"label": "Details", "page": 3, "items": {"P3_EMPNO": "&P22_EMPNO."}},
37
- {"label": "Start over", "request": "RESET", "confirm": "Clear the form?", "authz": "ADMIN"}]
37
+ {"label": "Start over", "request": "RESET", "confirm": "Clear the form?", "authz": "ADMIN"},
38
+ {"label": "Export", "action": "EXPORT", "icon": "download"}]
38
39
  ```
39
40
 
40
41
  | Key | Meaning |
@@ -42,6 +43,7 @@ the application or a **submit request**:
42
43
  | `label` | The entry's text (1–100 characters) |
43
44
  | `page`, `items` | A link: the page and the items to set (signed with a checksum, like any link) |
44
45
  | `request` | A submit: the page is submitted with this request, as if a button of that name was pressed. Validations, processes and branches with `when_button` see it |
46
+ | `action` | Runs the [dynamic actions of event `menu_entry`](07-dynamic-actions.md#menu-entry-chosen) with this name (capitals, digits and `_`) as trigger (APEX: Menu entry chosen) |
45
47
  | `confirm` | A confirmation question first |
46
48
  | `authz` | The entry only exists for users who pass this authorization scheme |
47
49
  | `icon` | An [icon](09-reference.md#icons) name |
@@ -852,6 +854,9 @@ application ask someone to approve, reject or complete something, and act on the
852
854
  | When overdue, Maximum renewals, Escalation role | The expiration policy ([below](#overdue-tasks-expire-renew-escalate)); empty: overdue tasks stay open |
853
855
  | Details page, Details item | The subject links to this page, setting the item to the record key |
854
856
  | On completion (SQL) | Runs as the application's role when the task is approved, rejected or completed (below) |
857
+ | Outcomes | Custom outcomes instead of Approve / Reject ([below](#custom-outcomes)) |
858
+ | Events, When the completion SQL fails | Actions when a task is completed, expires or its completion SQL fails ([below](#task-events-and-notifications)) |
859
+ | Notifications | E-mail templates pgkiln sends when a task is created, completed or expires |
855
860
 
856
861
  **2. Application SQL creates tasks**, for example in the process that saves a request or in a
857
862
  trigger:
@@ -903,6 +908,62 @@ When a record is decided somewhere else (for example with buttons on its own pag
903
908
  open tasks so they don't linger: `select meta.close_tasks('EXPENSE_APPROVAL', :P5_ID::text,
904
909
  'approved')` (no completion SQL runs).
905
910
 
911
+ <a id="custom-outcomes"></a>**Custom outcomes** (APEX 26.2: task outcomes). A task definition may list its own outcomes
912
+ instead of Approve / Reject (or Complete):
913
+
914
+ ```json
915
+ [{"name": "YES", "label": "Yes", "colour": "success"},
916
+ {"name": "NO", "label": "No", "colour": "danger"},
917
+ {"name": "UNDECIDED", "label": "Undecided", "colour": "warning"}]
918
+ ```
919
+
920
+ The task list then shows one button per outcome, with its label and colour (`default`, `primary`, `success`,
921
+ `danger` or `warning`: a fixed list, so a definition can't add other classes). The outcome is stored in lower case
922
+ (`meta.tasks.outcome = 'yes'`), `:OUTCOME` in the completion SQL is the name (`YES`), and the history shows the label.
923
+ A task ends only with an outcome of its definition: `meta.complete_task` refuses any other (also Approve / Reject
924
+ on a definition with its own outcomes), and so does `meta.close_tasks(name, detail_pk, outcome)`. A
925
+ [workflow](#workflows) branches on them in a task step's `next` (`{"yes": "PAY", "no": "END"}`) or a switch case
926
+ `{"outcome": "UNDECIDED", "next": "ASK_AGAIN"}`. Names are letters, digits and underscores (at most 30, at most 10
927
+ outcomes); `cancelled`, `expired` and `default` are reserved.
928
+
929
+ <a id="task-events-and-notifications"></a>**Task events and notifications** (APEX 26.2: Human Task events). A
930
+ definition's *Events* are actions on what happens to its tasks:
931
+
932
+ ```json
933
+ [{"on": "complete", "type": "sql", "code": "select expenses.log_decision(:DETAIL_PK::int, :OUTCOME, :APPROVER)"},
934
+ {"on": "expire", "type": "mail", "to": "select email from staff where role = 'manager'", "template": "TASK_EXPIRED"},
935
+ {"on": "fail", "type": "workflow", "workflow": "FIX_REQUEST", "vars": {"REASON": "&ERROR."}}]
936
+ ```
937
+
938
+ | `on` | When |
939
+ |---|---|
940
+ | `complete` | The task was completed, with any outcome (also by `meta.close_tasks` with an outcome) |
941
+ | `expire` | The task expired ([below](#overdue-tasks-expire-renew-escalate)) |
942
+ | `fail` | The definition's completion SQL raised an error |
943
+
944
+ | `type` | Does | Fields |
945
+ |---|---|---|
946
+ | `sql` | Runs statements as the application's role | `code` |
947
+ | `mail` | Queues an e-mail through [`meta.send_mail_template`](#e-mail-templates) or [`meta.send_mail`](#sending-e-mail) | `to` (a SELECT returning addresses, or addresses with `&NAME.`), `template`, or `subject` and `body` with `#NAME#` placeholders |
948
+ | `workflow` | Starts a workflow with the task's record key and parameters | `workflow`, `vars` (variable → text, `&NAME.` allowed) |
949
+
950
+ Binds (and `#NAME#` placeholders): the task's parameters, `:TASK_ID`, `:DETAIL_PK`, `:SUBJECT`, `:OUTCOME`,
951
+ `:APPROVER`, `:INITIATOR`, `:EVENT`, and for `fail` `:ERROR` and `:COMMENT`. *Complete* and *expire* events run
952
+ right after the commit: a trigger queues them (`meta.task_hook`, closed to applications) and the server runs each
953
+ in a transaction of its own, as the application's role with the person who completed the task (or the initiator) as
954
+ `meta.app_user()` and the task's tenant; one that fails is tried three times and keeps its error on the
955
+ `meta.task_hook` row. *Fail* events run in the request that completed the task: the completion SQL's effect is
956
+ undone, the failure is in the task's history, the actions run, and **When the completion SQL fails** says what
957
+ happens to the task: `reopen` (the default) undoes the decision so the task stays open, `complete` keeps it completed
958
+ without the SQL's effect. The user sees the error either way. Without fail events or a recovery, an error undoes the
959
+ decision as before.
960
+
961
+ **Notifications** are e-mails pgkiln sends itself, from the application's
962
+ [e-mail templates](#e-mail-templates): `{"create": "TASK_CREATED", "complete": "TASK_DONE", "expire": "TASK_EXPIRED"}`.
963
+ *create* goes to the potential owners (the named users, the actual owner and the application's users with one of
964
+ the owner roles), *complete* to the initiator, *expire* to both, at the e-mail address of their account. The
965
+ placeholders are those of the events. They need a mail service, like `meta.send_mail`.
966
+
906
967
  #### Overdue tasks: expire, renew, escalate
907
968
 
908
969
  A task definition with *Due in* can say what happens to a task that is still open after its due date (APEX:
@@ -930,9 +991,9 @@ workflow whose task is cancelled.
930
991
  | `meta.task_events` | View: the history and comments of those tasks |
931
992
 
932
993
  The task tables themselves are closed to the application's role: rights are checked by these
933
- functions and views, for every call. To tell owners by e-mail, call
934
- [`meta.send_mail`](#sending-e-mail) from the task definition's SQL or from an automation that
935
- reminds owners of overdue tasks; or put a task list on the home page.
994
+ functions and views, for every call. To tell owners by e-mail, use the definition's
995
+ [notifications](#task-events-and-notifications), call [`meta.send_mail`](#sending-e-mail) from the task
996
+ definition's SQL or from an automation that reminds owners of overdue tasks; or put a task list on the home page.
936
997
 
937
998
  **Example:** in the HR sample application, a leave request creates a `LEAVE_APPROVAL` task for the
938
999
  employee's manager; *My tasks* (page 14) approves or rejects it through `hr.decide_leave`, the same
@@ -954,9 +1015,15 @@ steps; the builder checks them and draws the flow.
954
1015
  | `invoke_api` | Calls a REST data source or a URL on the server; values of the response become variables ([below](#invoke-api-steps)) | `source` + `params`, or `url`, `method`, `credential`, `body`; `variables`, `status_variable`, `response_variable`, `timeout`, `next` |
955
1016
  | `parallel` | Starts a branch at each of its steps; they run side by side | `branches` (the first step of each, at least two), `join` |
956
1017
  | `join` | Where the branches of a `parallel` step meet | `wait_for` (`all`, the default, or `any`), `next` |
1018
+ | `send_mail` | Queues an e-mail ([below](#send-e-mail-steps)) | `to`, `template` (or `subject` and `body`), `cc`, `next` |
1019
+ | `draft` | A placeholder while modelling: only in a development version ([below](#draft-steps)) | `note`, `next` |
957
1020
  | `end` | Ends the workflow | |
958
1021
 
959
1022
  `next` is optional: the following step in the list (after the last one, the workflow is complete).
1023
+ A task step's `next` per outcome names the outcomes of its task definition, [custom ones](#custom-outcomes) too,
1024
+ plus `cancelled`, `expired` and `default`. A `switch` case is either `{"when": "SQL condition", "next": …}` or
1025
+ `{"outcome": "YES", "next": …}`, which compares the outcome of the last task (`:TASK_OUTCOME`); the builder checks
1026
+ that a task of the workflow can end with it.
960
1027
 
961
1028
  ```json
962
1029
  [{"name": "CHECK", "type": "switch", "cases": [{"when": ":AMOUNT::numeric > 1000", "next": "DIRECTOR"}], "otherwise": "MANAGER"},
@@ -976,7 +1043,7 @@ select meta.start_workflow('EXPENSE', :P5_ID, jsonb_build_object('AMOUNT', :P5_A
976
1043
 
977
1044
  The variables (upper case) are binds in every step, with `:DETAIL_PK`, `:WORKFLOW_ID` and
978
1045
  `:INITIATOR`; after a task step also `:TASK_OUTCOME` (`APPROVED`, `REJECTED`, `COMPLETED`,
979
- `CANCELLED`) and `:TASK_APPROVER`. The title may use `&VAR.`.
1046
+ `CANCELLED`, `EXPIRED` or a custom outcome such as `YES`) and `:TASK_APPROVER`. The title may use `&VAR.`.
980
1047
 
981
1048
  **How it runs.** The pgkiln server runs workflows: right after they start or a task of theirs
982
1049
  ends (`NOTIFY`), and it checks for waits that are over every few seconds (`WORKFLOW_INTERVAL_S`,
@@ -1008,6 +1075,29 @@ background runs with the tenant of the session that queued it. Filter your own t
1008
1075
  (`where tenant_id = meta.tenant_id()`, or a row level security policy). Use row level security in
1009
1076
  the tenant's tables as well: the tenant is a value the application sets, not a database boundary.
1010
1077
 
1078
+ ### Send e-mail steps
1079
+
1080
+ A `send_mail` step (APEX 26.1: the *Send E-Mail* activity) queues an e-mail through the application's mail service,
1081
+ as the application's role, like [`meta.send_mail`](#sending-e-mail) in a `sql` step:
1082
+
1083
+ ```json
1084
+ {"name": "TELL", "type": "send_mail", "to": "select email from staff where id = :DETAIL_PK::int", "template": "REQUEST_DONE"}
1085
+ {"name": "NOTE", "type": "send_mail", "to": "desk@example.com, &MANAGER_EMAIL.", "subject": "Request #TITLE# done", "body": "Decided by #TASK_APPROVER#."}
1086
+ ```
1087
+
1088
+ `to` is a SELECT returning addresses, or addresses with `&VAR.` substitutions; `cc` likewise addresses. With a
1089
+ `template` ([e-mail templates](#e-mail-templates)), or a `subject` and a plain-text `body`, the placeholders `#NAME#`
1090
+ are the variables, `DETAIL_PK`, `WORKFLOW_ID` and `INITIATOR`. No recipient: the step goes on (the history says
1091
+ so). A mail that can't be queued (no mail service, an address that isn't one, the daily limit) faults the step.
1092
+ The history records the mail's id; it is sent after the step's transaction commits.
1093
+
1094
+ <a id="draft-steps"></a>**Draft steps** (APEX 26.2: Draft activities). A step of type `draft` stands for something
1095
+ not modelled yet (`{"name": "BUDGET_CHECK", "type": "draft", "note": "check the budget", "next": "APPROVE"}`).
1096
+ It may be in a development version only: the builder shows a warning and doesn't offer **Activate** while a version
1097
+ has one, `meta.activate_workflow_version` refuses it, and the database refuses draft steps in a definition's active
1098
+ steps (so a new definition can't have them either: create it, then a new version to model with drafts). The diagram
1099
+ draws them dashed. An instance can never reach one; if it did (steps changed with SQL), the step faults.
1100
+
1011
1101
  ### Invoke API steps
1012
1102
 
1013
1103
  An `invoke_api` step (APEX: the *Invoke API* activity) calls a web service: a
@@ -1086,6 +1176,28 @@ steps with problems), and the active one becomes inactive. **Discard** throws a
1086
1176
  away. The Versions table shows each version with its instance count and diagram; the Instances
1087
1177
  table shows which version each instance runs and where it is.
1088
1178
 
1179
+ <a id="migrating-instances"></a>**Migrating running instances** (APEX 26.2: in-flight workflow migration). Running
1180
+ instances (active, waiting or faulted) of an older version can move to a newer one: the active version or an inactive
1181
+ version activated after theirs. Under the definition, **Migrate instances…** (shown
1182
+ while there are such instances) opens a page where you choose the version and the instances, and map each step an
1183
+ instance is at to a step of the new version (a step of the same name is proposed). **Preview** shows per instance
1184
+ where it goes or why it can't; **Migrate** then moves them all in one transaction, with their variables, the task
1185
+ they wait for and their wait, logged in each instance's history (*migrated*, `version 1 → 2: APPROVE → APPROVE
1186
+ (by admin)`) and in `meta.workflow_migration`. Nothing moves when one of them can't:
1187
+
1188
+ - a step without a mapping, or mapped to a step the version doesn't have (or a draft);
1189
+ - an instance waiting for a task must go to a task step that creates a task of the same definition;
1190
+ one at a `wait` step to a `wait` step (its wait goes on), one at a join to a join;
1191
+ - an instance inside parallel branches, or calling a web service at that moment: try again later.
1192
+
1193
+ The database function is `meta.migrate_workflows(definition_id, version, ids, mapping, apply, by)` (the owner's,
1194
+ for the builder and scripts; not for applications).
1195
+
1196
+ <a id="diagram-export"></a>**The diagram as a file.** Under the diagram, **Download SVG** downloads the whole model
1197
+ of the version shown (`/builder/apps/<app>/shared/workflow_definition/<id>/diagram.svg?v=<version>`), a standalone
1198
+ SVG with its own colours; **Download PNG** draws that SVG in the browser (with JavaScript; without it the SVG link
1199
+ remains). Step names and labels are escaped, so a definition can't put markup into the file.
1200
+
1089
1201
  **The console.** A region of type **`workflows`** lists the workflows the user started (or, with
1090
1202
  `"context": "admin"`, those they administer), with their state, the version, the current steps (several with parallel branches), a diagram
1091
1203
  and the history. The initiator and administrators can **terminate** a workflow (its open tasks and
@@ -1102,7 +1214,10 @@ branches are cancelled); administrators can retry a faulted one.
1102
1214
  created. Its version 2 (part 18) prepares the workplace (the manager's task) and, in a parallel
1103
1215
  branch, the access that employees with a salary of 2500 or more need (a switch and an
1104
1216
  administrator's task); when both branches are done, the manager is notified (SQL). Version 1, which
1105
- did this one after the other, is inactive. *My tasks* (page 14) shows the workflows. Part 34 adds
1217
+ did this one after the other, is inactive. *My tasks* (page 14) shows the workflows. Part 67 adds `SALARY_CHANGE`: a
1218
+ `SALARY_DECISION` task with the outcomes Yes, No and Undecided (the owners get the `SALARY_TASK` e-mail, a complete
1219
+ event notifies the initiator), a switch on the outcome (Undecided asks again), SQL that applies the salary and, in
1220
+ version 2, a `send_mail` step to the employee; version 3 is in development with a draft step. Part 34 adds
1106
1221
  `DEPARTMENT_CHECK`, started by *Check in a workflow* on page 23: an `invoke_api` step reads the
1107
1222
  department from the example's own REST API (the `DEPARTMENT` source of part 23, so the server must
1108
1223
  allow `127.0.0.1:3100`), a `switch` on `HTTP_STATUS`, and SQL that notifies the initiator.
@@ -37,6 +37,7 @@ your own from a static application file.
37
37
  | `selection_change` | Rows are selected in a report with [row selection](04-pages-and-regions.md#row-selection), or another master row in a grid; the condition sees the selected values, colon separated | Region ids or static ids |
38
38
  | `before_submit` | The page is about to be submitted; a declined `confirm` keeps the page | – |
39
39
  | `custom` | JavaScript raised the event: `pgkiln.trigger('ORDER_SAVED', value)` (APEX: custom events, `apex.event.trigger`) | Event names (case-insensitive) |
40
+ | `menu_entry` | An entry `{"label": …, "action": "NAME"}` of a [menu button](06-processing.md#menu-buttons-and-badges) was chosen (APEX: Menu entry chosen); the condition sees the entry's name ([below](#menu-entry-chosen)) | Entry names, or menu button names (any of their entries) |
40
41
 
41
42
  **The actions of one event and trigger run one after the other, in sequence order** (APEX: the true
42
43
  actions of a dynamic action), each waiting for the one before (a server action for its answer). A
@@ -79,11 +80,44 @@ actions of a dynamic action), each waiting for the one before (a server action f
79
80
  | `print` | browser | The browser's print dialog |
80
81
  | `current_position` | browser | The device's position as `latitude,longitude` (5 decimals) into the first affected item |
81
82
  | `share` | browser | The device's share sheet with the page's title, `message` and address; where there is none, the address is copied |
83
+ | `wait_popup` | browser | Shows a modal "please wait" popup with `message` (default *Please wait…*) until `remove_wait_popup` or the next page (APEX: Show Wait Popup); [below](#wait-popup) |
84
+ | `remove_wait_popup` | browser | Removes the wait popup (APEX: Remove Wait Popup) |
82
85
 
83
86
  Server-side actions first store `items_to_submit` in session state, run as the application's
84
87
  database role like everything else, and check the page's and the dynamic action's authorization.
85
88
  The SQL itself never reaches the browser.
86
89
 
90
+ ## Wait popup
91
+
92
+ `wait_popup` covers the page with a small dialog (a spinner and `message`) while something slow
93
+ runs, so the user doesn't click twice; `remove_wait_popup` takes it away. Because the actions of
94
+ one event run in sequence, each waiting for the one before, two patterns cover most cases:
95
+
96
+ | Sequence | Event | Action | |
97
+ |---|---|---|---|
98
+ | 10 | `click` on `RECALC` (a submit button) | `wait_popup` | *Recalculating…*: the popup stays until the next page arrives |
99
+ | 10 | `click` on `CHECK` | `wait_popup` | *Checking…* |
100
+ | 20 | `click` on `CHECK` | `execute_sql` | the Ajax call (also `set_value`, `refresh_region`, `download`, a plug-in) |
101
+ | 30 | `click` on `CHECK` | `remove_wait_popup` | |
102
+
103
+ `before_submit` → `wait_popup` shows it for every submit of the page. A submit the browser refuses
104
+ (a required field is empty) removes it again, and so does going back to the page in the browser.
105
+ The popup is a modal `<dialog>`: the page below can't be clicked or focused, Escape doesn't close
106
+ it, its text is a status message that screen readers read out, and the page's `<main>` is
107
+ `aria-busy` while it shows. It follows the theme (light and dark) and needs no inline styles or
108
+ scripts. From JavaScript: `const done = pgkiln.waitPopup('Saving…'); … done();` (or
109
+ `pgkiln.removeWaitPopup()`).
110
+
111
+ ## Menu entry chosen
112
+
113
+ A [menu button](06-processing.md#menu-buttons-and-badges)'s entry can run dynamic actions instead
114
+ of linking or submitting: `{"label": "Export", "action": "EXPORT"}`. Dynamic actions with event
115
+ `menu_entry` and trigger `EXPORT` run when it is chosen (the menu closes first); a trigger with the
116
+ menu button's name runs for every action entry of that menu, and a client-side condition tests the
117
+ entry's name (`equals` `EXPORT`). Without JavaScript such an entry does nothing, like a button with
118
+ action `da`. The HR example's page 47 has an *Actions* menu whose *Count employees* entry shows a
119
+ wait popup around an Ajax call.
120
+
87
121
  ## Ajax callbacks
88
122
 
89
123
  A page process with point **ajax** (APEX: an On Demand process) runs only when JavaScript calls it by
@@ -481,9 +481,10 @@ navigation entries and application processes have the same `build_option` column
481
481
  | `push_key` | The VAPID key pair of an application with [push notifications](17-mobile.md#push-notifications): `public_key`, `private_key` (encrypted with `PGKILN_SECRET_KEY`). Not exported | no |
482
482
  | `push_subscription` | Devices with notifications on: `app_id`, `username`, `endpoint` (the push service URL), the device's keys `p256dh` and `auth`, `user_agent`, `created_at`, `last_sent_at`, `failures`. Not exported | no (through `meta.has_push_subscription`) |
483
483
  | `push_message` | [Push notifications from SQL](#push-notifications-from-sql): `app_id`, `username`, `title`, `body`, `url`, `tag`, `urgency`, `ttl_s`, `status`, `attempts`, `devices`, `delivered`, `message`, `requested_by`, times. Kept 7 days, not exported | no |
484
- | `ai_service` | [AI services](06-processing.md#generate-text-with-ai) of the installation: `name`, `provider` (`anthropic`, `openai`), `model`, `effort`, `refusal_fallback`, `max_tokens`, `timeout_s`, `base_url`, `api_key_enc` (encrypted with `PGKILN_SECRET_KEY`, write-only), `enabled`. Not exported | no |
484
+ | `ai_service` | [AI services](06-processing.md#generate-text-with-ai) of the installation: `name`, `provider` (`anthropic`, `openai`, `gemini`, `openai_compatible`: 121), `model`, `effort`, `refusal_fallback`, `max_tokens`, `timeout_s`, `base_url`, `api_key_enc` (encrypted with `PGKILN_SECRET_KEY`, write-only), `enabled`. Not exported | no |
485
485
  | `app_ai_service` | Which applications may use which AI service, with daily limits `max_requests` and `max_tokens` (null: no limit). Not exported | no (through `meta.ai_available`) |
486
486
  | `ai_usage` | One row per AI request: `at`, `app_id`, `page_no`, `username`, `service`, `provider`, `model`, `source`, `input_tokens`, `output_tokens`, `duration_ms`, `status`, `message` (an error class, never prompt or answer text). Not exported | no |
487
+ | `ai_tool_call` | (121) Every tool call of an [AI assistant or agent](03-builder.md#ai-agents): `at`, `app_id`, `page_no`, `region_id`, `username`, `agent`, `tool`, `type`, `arguments` (the model's, cut to 4 KB), `status` (`ok`, `error`, `refused`), `duration_ms`, `message`. Kept 30 days, not exported | no |
487
488
  | `ai_conversation` | [AI assistant](04-pages-and-regions.md#ai_assistant-ai-assistant) conversations: one per session and region (`messages`: the provider's history, `turns`: what the page shows); deleted with the session. Not exported | no |
488
489
  | `ai_table_note` | Descriptions of tables and columns for models ([App Builder AI](03-builder.md#app-builder-ai)): `schema_name`, `table_name`, `column_name` (`''` for the table), `note`. Installation data, not exported | no |
489
490
  | `blueprint` | Saved [blueprints](03-builder.md#creating-an-application-from-a-blueprint): `name`, `spec` (the JSON), `app_id` (the application last created from it), `created_by`. Builder data, not exported | no |
@@ -22,10 +22,11 @@ What's missing is listed in the [feature parity matrix](../apex-feature-parity.m
22
22
  | Instance administration: install/upgrade log | Workspace utilities → Installation (administrators) |
23
23
  | `APEX_WEB_SERVICE.make_rest_request`, `g_status_code` | `meta.web_request(url, method, body, headers, credential)` or `meta.web_request_source(source, params)`, then `meta.web_response(id)`. **Asynchronous**: the server makes the request after the page process that queued it, or after the commit ([chapter 9](09-reference.md#web-requests-from-sql)) |
24
24
  | Workspace → Generative AI services; *Generate Text with AI* process and dynamic action; `APEX_AI.generate` | Workspace utilities → AI services (Claude or OpenAI; administrators); the `ai_generate` process and dynamic action, with structured outputs into items ([chapter 6](06-processing.md#generate-text-with-ai)); `meta.ai_generate(service, prompt, system, schema)` then `meta.ai_result(id)` ([chapter 9](09-reference.md#ai-requests-from-sql)) |
25
- | AI Assistant region; AI agents with tools; natural language to interactive report (NL2IR) | The `ai_assistant` region: a chat with context queries (RAG over your queries) and tools (SQL as the app role with bound arguments, REST data sources) ([chapter 4](04-pages-and-regions.md#ai_assistant-ai-assistant)); `"ai_filter"` on a report: *Ask in your own words* ([chapter 4](04-pages-and-regions.md#natural-language-filters-ask-in-your-own-words)) |
25
+ | AI Assistant region; AI agents with tools; AI interactive reports (natural language to interactive report) | The `ai_assistant` region: a chat with context queries (RAG over your queries) and tools (SQL as the app role with bound arguments, REST data sources) ([chapter 4](04-pages-and-regions.md#ai_assistant-ai-assistant)); `"ai_filter"` on a report: *Ask in your own words*, the request becomes the report's settings as editable chips, with or without an AI service ([chapter 4](04-pages-and-regions.md#ask-in-your-own-words-ai-interactive-reports)) |
26
26
  | App Builder / SQL Workshop AI Assistant (create pages, generate and explain SQL); describing tables for AI | The App Builder's AI service: *Create pages with AI* (proposals you confirm), SQL Workshop → AI (SQL from a question, shown not run; explain a query or error), *Describe tables* (`meta.ai_table_note`, optionally `COMMENT ON`) ([chapter 3](03-builder.md#app-builder-ai)) |
27
27
  | Blueprints (spec-driven development) | Create → From a blueprint: a JSON blueprint (tables, pages, menu, sample data), optionally drafted by AI, reviewed, then created in one transaction ([chapter 3](03-builder.md#creating-an-application-from-a-blueprint)) |
28
28
  | `APEX_DATA_PARSER.parse`, `get_columns` | `meta.parse_data(blob, …)` (rows with `cols` and `data`), `meta.parse_data_columns(blob, …)`; CSV/TSV and JSON, not Excel or XML ([chapter 9](09-reference.md#parsing-files-in-sql)) |
29
+ | Team Development (issues, labels, milestones, templates, feedback) | Builder → Team Development (`/builder/team`): issues per workspace with labels in groups, milestones, assignees, application and page links, Markdown comments with @mentions shown in the builder, history, saved views and a board; a set-up wizard with sample labels, templates and milestones; feedback and developer comments become issues. No e-mail and no GitHub synchronisation ([chapter 3](03-builder.md#team-development)) |
29
30
  | Workspace users (APEX accounts), Application Access Control | `meta.account` (Builder → Users), `meta.app_access` (Access control) |
30
31
  | Page access protection "Arguments must have checksum" | `protection = 'checksum'` (the default) |
31
32
  | Automatic row processing (DML) | Process type `form_dml` |