pgkiln 0.51.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 (89) hide show
  1. package/SECURITY.md +5 -0
  2. package/db/migrations/121_ai_agents.sql +94 -0
  3. package/db/migrations/122_team_development.sql +186 -0
  4. package/db/migrations/123_workflow_outcomes.sql +407 -0
  5. package/db/migrations/124_component_details.sql +51 -0
  6. package/docs/apex-feature-parity.md +12 -10
  7. package/docs/guide/01-installation.md +7 -3
  8. package/docs/guide/03-builder.md +121 -7
  9. package/docs/guide/04-pages-and-regions.md +71 -21
  10. package/docs/guide/05-items.md +7 -1
  11. package/docs/guide/06-processing.md +123 -8
  12. package/docs/guide/07-dynamic-actions.md +34 -0
  13. package/docs/guide/09-reference.md +2 -1
  14. package/docs/guide/11-from-apex.md +2 -1
  15. package/docs/guide/12-development.md +44 -8
  16. package/docs/guide/20-ai-agents.md +4 -2
  17. package/examples/hr/README.md +2 -0
  18. package/examples/hr/hr_64_ai_agent.sql +97 -0
  19. package/examples/hr/hr_65_ask_reports.sql +21 -0
  20. package/examples/hr/hr_66_team_development.sql +58 -0
  21. package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
  22. package/examples/hr/hr_68_component_details.sql +96 -0
  23. package/package.json +1 -1
  24. package/public/app.css +40 -0
  25. package/public/app.js +134 -20
  26. package/public/builder.css +83 -1
  27. package/public/builder.js +47 -0
  28. package/src/ai/chat.ts +18 -18
  29. package/src/ai/service.ts +35 -11
  30. package/src/ai/types.ts +7 -2
  31. package/src/app.ts +6 -0
  32. package/src/builder/advisor.ts +6 -3
  33. package/src/builder/ai-agents.ts +372 -0
  34. package/src/builder/ai-builder.ts +3 -3
  35. package/src/builder/ai.ts +13 -9
  36. package/src/builder/assistant.ts +22 -7
  37. package/src/builder/components.ts +38 -9
  38. package/src/builder/designer.ts +8 -3
  39. package/src/builder/feedback.ts +6 -1
  40. package/src/builder/locks.ts +7 -1
  41. package/src/builder/region-help.ts +2 -2
  42. package/src/builder/routes.ts +4 -1
  43. package/src/builder/shared.ts +1 -1
  44. package/src/builder/team.ts +991 -0
  45. package/src/builder/ui.ts +2 -1
  46. package/src/builder/workflow-migrate.ts +210 -0
  47. package/src/builder/workflows.ts +29 -6
  48. package/src/builder/workspace-home.ts +2 -0
  49. package/src/builder/workspace-transfer.ts +1 -1
  50. package/src/cli/replace.ts +7 -1
  51. package/src/i18n/ar.ts +17 -0
  52. package/src/i18n/cs.ts +17 -0
  53. package/src/i18n/da.ts +17 -0
  54. package/src/i18n/de.ts +17 -0
  55. package/src/i18n/el.ts +17 -0
  56. package/src/i18n/es.ts +17 -0
  57. package/src/i18n/fi.ts +17 -0
  58. package/src/i18n/fr.ts +17 -0
  59. package/src/i18n/he.ts +17 -0
  60. package/src/i18n/it.ts +17 -0
  61. package/src/i18n/ja.ts +17 -0
  62. package/src/i18n/ko.ts +17 -0
  63. package/src/i18n/nb.ts +17 -0
  64. package/src/i18n/pl.ts +17 -0
  65. package/src/i18n/pt.ts +17 -0
  66. package/src/i18n/ru.ts +17 -0
  67. package/src/i18n/sv.ts +17 -0
  68. package/src/i18n/tr.ts +17 -0
  69. package/src/i18n/uk.ts +17 -0
  70. package/src/i18n/zh.ts +17 -0
  71. package/src/i18n.ts +34 -0
  72. package/src/instance.ts +2 -0
  73. package/src/metadata.ts +9 -3
  74. package/src/runtime/ai-agents.ts +365 -0
  75. package/src/runtime/ai-filter.ts +385 -107
  76. package/src/runtime/assistant.ts +64 -11
  77. package/src/runtime/context.ts +2 -0
  78. package/src/runtime/ir-phrases.ts +812 -0
  79. package/src/runtime/ir-plan.ts +449 -0
  80. package/src/runtime/items.ts +1 -1
  81. package/src/runtime/regions.ts +6 -2
  82. package/src/runtime/render.ts +1 -1
  83. package/src/runtime/report.ts +1 -1
  84. package/src/runtime/tasks.ts +63 -12
  85. package/src/task-events.ts +232 -0
  86. package/src/team-samples.ts +74 -0
  87. package/src/team.ts +342 -0
  88. package/src/workflow.ts +135 -18
  89. package/src/workspace-export.ts +16 -1
@@ -182,6 +182,8 @@ which is read at startup; real environment variables take precedence.
182
182
  | `PDF_FONT`, `PDF_FONT_BOLD` | *(none)* | TrueType fonts for report PDFs, for text beyond Western European (e.g. `DejaVuSans.ttf`) |
183
183
  | `ANTHROPIC_API_KEY` | *(none)* | Default API key of [AI services](03-builder.md#ai-services) with the provider Claude that have no key of their own |
184
184
  | `OPENAI_API_KEY` | *(none)* | Default API key of AI services with the provider OpenAI that have no key of their own |
185
+ | `GEMINI_API_KEY` | *(none)* | (sprint 111) Default API key of AI services with the provider Gemini that have no key of their own |
186
+ | `OPENAI_COMPATIBLE_API_KEY` | *(none)* | (sprint 111) Default API key of OpenAI-compatible AI services (Ollama, vLLM, Azure OpenAI) that have no key of their own; without either, no key is sent (`none`) |
185
187
  | `PGKILN_SECRET_KEY` | *(none)* | Encrypts the secrets of web credentials and the API keys of AI services; at least 32 characters (e.g. `openssl rand -base64 32`). Keep it outside the database; changing it means entering the secrets again ([chapter 19](19-rest-data-sources.md)) |
186
188
  | `PGKILN_REST_ALLOWED_HOSTS` | *(none: no outgoing calls)* | Hosts REST data sources and `invoke_api` may call: `api.example.com`, `*.example.com`, `host:8443`, `*` (any public host) ([chapter 19](19-rest-data-sources.md#server-configuration-and-the-allow-list)) |
187
189
  | `PGKILN_REST_PRIVATE_HOSTS` | *(none)* | Hosts that may resolve to private, loopback or link-local addresses (also allows them) |
@@ -264,9 +266,11 @@ stops the container at once with a message saying so.
264
266
  | `PGKILN_IMAGE` | `pgkiln/pgkiln:latest` | The image to run, e.g. a fixed version (`pgkiln/pgkiln:0.31.0`) or `ghcr.io/pgkiln/pgkiln` |
265
267
 
266
268
  **Upgrading:** with the published image: `docker compose pull`, then `docker compose up -d` (with a
267
- fixed version in `PGKILN_IMAGE`, change it first). Built from a checkout: `git pull`, then
268
- `docker compose up -d --build`. The new container migrates the database
269
- before it starts serving. Back up first (`docker compose exec db pg_dump -U pgkiln pgkiln > backup.sql`).
269
+ fixed version in `PGKILN_IMAGE`, change it first); `up -d` replaces the container when the image changed, so
270
+ `--force-recreate` isn't needed. Built from a checkout: `git pull`, then `docker compose up -d --build`. The new
271
+ container migrates the database before it starts serving. A `compose.yaml` from pgkiln 0.51.0 or older has
272
+ `pull_policy: missing` under `pgkiln`, which makes `docker compose pull` skip an image that is already present:
273
+ delete that line (or take the current `deploy/compose.yaml`) before the first upgrade this way. Back up first (`docker compose exec db pg_dump -U pgkiln pgkiln > backup.sql`).
270
274
  Keep `PGKILN_SECRET_KEY`: without it, stored secrets (web credentials, AI keys, push keys) can't be decrypted.
271
275
 
272
276
  ## Upgrading
@@ -158,6 +158,7 @@ applications' files. Choose what goes in:
158
158
  | REST source catalogs | Every [catalog](19-rest-data-sources.md#rest-source-catalogs) with its services |
159
159
  | SQL scripts, blueprints, data generators | The SQL Workshop's saved work |
160
160
  | Data Reporter datasets | Every [dataset](#data-reporter-datasets-and-reporting-applications) (sprint 110) |
161
+ | Team Development | [Label groups, labels, milestones, templates and issues](#team-development) with their comments and history; an issue's application by alias (sprint 113) |
161
162
 
162
163
  Never in the file: the workspace's name, its [isolation](#isolated-workspaces) and database roles, passwords and
163
164
  secrets, script runs and other history. **Applications are exported on their own** (an application's *Export*).
@@ -275,7 +276,8 @@ JavaScript. The [default-password banner](#signing-in-and-developer-accounts) st
275
276
  - **System message**: the pgkiln version the server runs and, when the [release check](#the-release-check) is on and
276
277
  a newer release is out, **pgkiln X is available** with a link to its release notes and how to upgrade (back up,
277
278
  install the new version, `pgkiln migrate`, restart).
278
- - **A side column**: About, **Learn more** (this guide, the [tutorial](10-tutorial.md),
279
+ - **A side column**: About, **Team Development** ([open issues](#team-development), those assigned to you, unseen
280
+ mentions of you, or *Set up Team Development* before it is set up), **Learn more** (this guide, the [tutorial](10-tutorial.md),
279
281
  [Coming from APEX](11-from-apex.md), the changelog and the discussions on GitHub) and Resources.
280
282
 
281
283
  ### The release check
@@ -753,6 +755,10 @@ author or an administrator deletes it. The page list counts each page's comments
753
755
  another number keeps its lock and comments. Locks and comments belong to this installation and
754
756
  are not exported.
755
757
 
758
+ **Make an issue** under a comment turns it into a [Team Development](#team-development) issue about the same page;
759
+ the comment then links to its issue. A page with open issues shows them above the designer's tabs (*1 open issue:
760
+ …*, with *New issue* for that page), and the application's page list tags such pages (*2 issues*).
761
+
756
762
  ### Arranging the layout
757
763
 
758
764
  Regions are placed in sequence order; a region's *column span* (1–12) sets its width, and a
@@ -840,7 +846,7 @@ out, other keys are kept, columns the query no longer returns stay listed so you
840
846
  | `smart_filters` | The same per-column facets, the suggestions per facet and the search field's placeholder |
841
847
  | `display_selector` | Tabs or a select list, "Show all", remember the choice; per other region of the page: in a tab and the tab name (saved in that region's settings) |
842
848
  | `ai_assistant` | The AI service (those the application may use), system prompt, welcome text, placeholder, error message, tool rounds per question, questions per conversation, public or not, and the context queries and tools as JSON (checked when saving: query shapes, parameter types, REST data sources and authorization schemes that exist) ([AI assistant](04-pages-and-regions.md#ai_assistant-ai-assistant)) |
843
- | `report` (also) | *Ask in your own words (AI)*: the AI service and placeholder of the [natural-language filters](04-pages-and-regions.md#natural-language-filters-ask-in-your-own-words) |
849
+ | `report` (also) | *Ask in your own words (AI)*: off, the built-in phrases or an AI service, and the placeholder of [Ask in your own words](04-pages-and-regions.md#ask-in-your-own-words-ai-interactive-reports) |
844
850
  | `data_reporter` | Data sources (a table or view each: static id, label, description, offered columns with labels and format masks; or (sprint 110) a dataset of the workspace), who may share, build and moderate reports, publishing, rows per page ([Data Reporter](04-pages-and-regions.md#data_reporter-data-reporter)) |
845
851
  | `form_designer` | Who may design forms (an authorization scheme), data sources (a table or view each: static id, label, description, key column, may add / change / delete rows, offered columns with labels and lists of values), rows per page ([Form Designer](04-pages-and-regions.md#form_designer-form-designer)) |
846
852
 
@@ -958,6 +964,48 @@ user, page, rating and browser), set its status (*Open*, *In progress*, *Done*,
958
964
  an answer the user sees. Answering is allowed while the application is locked or Run only. The
959
965
  setting travels with the export; the feedback itself belongs to the installation.
960
966
 
967
+ **Make an issue** turns an entry into a [Team Development](#team-development) issue of the application's workspace
968
+ (title from its first line, the feedback quoted, linked to its page, labelled *Bug* for a problem and *Feature* for
969
+ an idea when the workspace has those labels); open feedback becomes *In progress*, and the entry links to its issue.
970
+
971
+ ## Team Development
972
+
973
+ (sprint 113, migration 122; APEX: Team Development) An issue tracker for the developers of a
974
+ [workspace](#workspaces): **Team Development** on the icon rail (`/builder/team`) and a block in the
975
+ [Workspace home](#workspace-home)'s side column with the number of open issues. Everything is of the current
976
+ workspace and works without JavaScript.
977
+
978
+ - **Set up** installs sample data, like APEX's set-up wizard: the label groups *Type* (Bug, Feature, Task) and
979
+ *Priority* (High, Medium, Low), issue templates (*Bug report*, *Feature request*, *Task*), canned responses and the
980
+ milestones *1.0*, *1.1* and *Backlog*. What the workspace has already (by name) is kept, so it can run again.
981
+ The samples are in English; other languages are added in `src/team-samples.ts`.
982
+ - **Issues** have a title, a description in Markdown (rendered safely: raw HTML is shown as text, links are http,
983
+ https, mailto or relative), *Open* or *Closed*, labels, a milestone, assignees (the workspace's developers and the
984
+ installation's administrators), and optionally an application of the workspace and one of its pages. Comments are
985
+ Markdown too; *Comment and close* does both. **@name** mentions a developer of the workspace: they see it under
986
+ *Mentions of you* (and on the Workspace home) until they open the issue or mark them seen; no mail is sent. Every
987
+ change is in the issue's history (who changed the title, labels, milestone, assignees, link or status, and when).
988
+ An issue is deleted by its author or a workspace administrator.
989
+ - **The list** filters by status, label, milestone, assignee (*Me*, *Nobody*), application and a search of the title
990
+ and text (or `#12` for issue 12). **Save view** keeps the filters under a name, for you only.
991
+ - **The board** shows the issues as columns of cards, by the labels of one label group (a card moved to *High*
992
+ loses *Medium*; *None of these* removes the group's labels) or by milestone. Each card has a *Move* form, so it
993
+ works without dragging.
994
+ - **Labels** belong to label groups and have a colour from a fixed palette (red, orange, yellow, green, teal, blue,
995
+ purple, gray). **Milestones** have a due date and are open or closed, with the share of their issues closed.
996
+ **Templates** are issue templates (title, labels, text: *Start from a template* on *New issue*) and canned
997
+ responses (*Canned response* → *Insert* above an issue's comment field). Any developer of the workspace adds and
998
+ changes them; only its administrators delete labels, label groups and milestones.
999
+ - Issues come from [feedback](#feedback) and [developer comments](#page-locks-and-comments) with **Make an issue**,
1000
+ linked both ways. The page designer shows a page's open issues and App Home counts them per page (*Open issues* in
1001
+ its Tasks).
1002
+
1003
+ Issues and the rest are workspace data: they are not part of an application's export (an in-place import keeps them)
1004
+ but travel in the workspace's [Export and import](#export-and-import) (*Team Development*): labels, milestones and
1005
+ templates match by name, an issue by its title and creation time; assignees are kept when they are developers of the
1006
+ importing workspace and the application link when it has an application with that alias. GitHub synchronisation is
1007
+ not part of it.
1008
+
961
1009
  ## Working copies
962
1010
 
963
1011
  APEX's working copies: a second application to change in isolation, then merge back. On an
@@ -1024,8 +1072,8 @@ is the earlier page: every component in one rail, with **Access control** next t
1024
1072
  | **Shortcuts** | Named texts: `"NAME"` in static content, help texts and confirm messages ([chapter 2](02-concepts.md#shortcuts-name)) |
1025
1073
  | **Application computations** | Computations of (application) items for every page: at sign-in, before every page, after every submit ([chapter 6](06-processing.md#application-computations)) |
1026
1074
  | **REST modules** | REST endpoints (method, path, SQL) served by pgkiln, with an OpenAPI description ([chapter 13](13-rest-api.md#rest-modules-in-the-builder)) |
1027
- | **Workflows** | Multi-step processes of tasks, SQL, web service calls (invoke API), decisions, waits and parallel branches, with versions and a diagram ([chapter 6](06-processing.md#workflows)) |
1028
- | **Task definitions** | Approvals and action tasks: subject, owners, administrators, due date, the SQL that runs on completion ([chapter 6](06-processing.md#approvals-and-the-task-list)) |
1075
+ | **Workflows** | Multi-step processes of tasks, SQL, web service calls (invoke API), e-mail, decisions, waits and parallel branches, with versions, draft steps, migration of running instances and a diagram (SVG / PNG download) ([chapter 6](06-processing.md#workflows)) |
1076
+ | **Task definitions** | Approvals and action tasks: subject, owners, administrators, due date, custom outcomes, the SQL that runs on completion, events and e-mail notifications ([chapter 6](06-processing.md#approvals-and-the-task-list)) |
1029
1077
  | **Document templates** | Letters, invoices and other PDFs filled from a query ([chapter 16](16-files.md#document-templates)), with a preview |
1030
1078
  | **Build options** | Include / exclude switches for features; pages and their components name one in their *Build option* property, and *Used in* lists them ([chapter 6](06-processing.md#build-options)) |
1031
1079
  | **Web credentials** | How the application signs in to web services (basic, API key header, bearer token, OAuth2 client credentials); the secret is write-only and encrypted ([chapter 19](19-rest-data-sources.md#web-credentials)) |
@@ -1293,7 +1341,12 @@ Workspace utilities → **AI services** (administrators only; APEX: *Generative
1293
1341
  large language model services of this installation. A service has:
1294
1342
 
1295
1343
  - a **name** (upper case, e.g. `CLAUDE`) by which processes, dynamic actions and SQL refer to it;
1296
- - a **provider**: *Claude (Anthropic)* or *OpenAI*, called through their official SDKs;
1344
+ - a **provider**: *Claude (Anthropic)* or *OpenAI*, called through their official SDKs; *Gemini*
1345
+ (Google, through its OpenAI-compatible endpoint `https://generativelanguage.googleapis.com/v1beta/openai/`,
1346
+ with the `openai` SDK); or *OpenAI-compatible*: any endpoint that speaks OpenAI's Chat Completions,
1347
+ such as Ollama (`http://localhost:11434/v1`), vLLM, Azure OpenAI (`https://<resource>.openai.azure.com/openai/v1/`)
1348
+ or a gateway. An OpenAI-compatible service needs a base URL; its key may stay empty for endpoints
1349
+ without keys (a local Ollama);
1297
1350
  - a **model**, exactly as the provider names it (new Claude services start with `claude-opus-5-5`);
1298
1351
  pgkiln never changes or downgrades it;
1299
1352
  - for Claude, the **effort** (how deeply the model thinks; current Claude models always think, lower
@@ -1305,8 +1358,9 @@ large language model services of this installation. A service has:
1305
1358
  - an optional **base URL** for a gateway or proxy that speaks the provider's API (calls to the AI
1306
1359
  service don't go through `PGKILN_REST_ALLOWED_HOSTS`: only administrators set this URL);
1307
1360
  - the **API key**: stored encrypted with `PGKILN_SECRET_KEY`, write-only (never shown again, never
1308
- exported or logged, not readable by applications). Without one the server's `ANTHROPIC_API_KEY` /
1309
- `OPENAI_API_KEY` is used.
1361
+ exported or logged, not readable by applications, never sent to the browser). Without one the
1362
+ server's `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY` or `OPENAI_COMPATIBLE_API_KEY`
1363
+ (by provider) is used.
1310
1364
 
1311
1365
  On a service's page, **Applications** sets which applications may use it, each with optional daily
1312
1366
  limits (requests and input + output tokens per UTC day; a request over a limit fails with a message
@@ -1314,11 +1368,71 @@ and is logged as *limited*), and **Test** sends a prompt (logged as a builder te
1314
1368
  on the list page for all services, the usage log: the last requests and the totals per application
1315
1369
  and service of the last 30 days.
1316
1370
 
1371
+ An interactive report's *Ask in your own words* doesn't need a service: without one (or when the
1372
+ service is disabled or not allowed for the application) pgkiln reads the request with its built-in
1373
+ phrases ([chapter 4](04-pages-and-regions.md#ask-in-your-own-words-ai-interactive-reports)).
1374
+
1317
1375
  **Prompts go to the provider.** Whatever a prompt contains, including the item values substituted
1318
1376
  into it, is sent to the chosen provider (or the gateway of the base URL). Choose providers and
1319
1377
  models that your organisation allows for that data. See
1320
1378
  [Generate text with AI](06-processing.md#generate-text-with-ai).
1321
1379
 
1380
+ ## AI agents
1381
+
1382
+ Shared Components → Generative AI → **AI Agents** (APEX 26.1 / 26.2: AI Agents and AI Tools) holds an
1383
+ application's agents and the tools they may use. An agent reasons over a user's request in an
1384
+ [AI assistant region](04-pages-and-regions.md#ai_assistant-ai-assistant) and acts **only through the
1385
+ tools you approve**; pgkiln prepares the context, runs each tool call itself under the checks below,
1386
+ and composes the conversation.
1387
+
1388
+ An **agent** has:
1389
+
1390
+ - a **name** (upper case, e.g. `SALES_AGENT`) that regions refer to, and a description;
1391
+ - an **AI service** the application may use, and a **system prompt** (`&ITEM.` is replaced by the item's
1392
+ value as delimited data, as in assistant regions);
1393
+ - a **reasoning effort**: *low*, *medium* or *high*, or empty for the service's own. For Claude it is
1394
+ the output effort (thinking depth, `output_config.effort`); for OpenAI, Gemini and OpenAI-compatible
1395
+ services it is sent as `reasoning_effort` (leave it empty for models without reasoning: they refuse it);
1396
+ - the **tools** it may use (ticked from the application's tools);
1397
+ - **maximum steps** (rounds of tool calls per question, 1–20, default 5) and **maximum output tokens**
1398
+ per call (never above the service's maximum);
1399
+ - an **authorization scheme**: other users see *You may not use this assistant* instead of the message
1400
+ box, and their questions are refused before anything is sent to the AI service.
1401
+
1402
+ A **tool** has a name the model calls it by, a description that tells the model what it does and when
1403
+ to use it, optional **parameters** (JSON: `{"DNAME": {"type": "string", "description": "…", "optional": true}}`;
1404
+ types string, integer, number, boolean, date, `enum` for a list of texts), an optional authorization
1405
+ scheme (only users who pass it are offered the tool) and one of these types:
1406
+
1407
+ | Type | What runs | Checks |
1408
+ |---|---|---|
1409
+ | **SQL query** | one `SELECT` (or `WITH`, `VALUES`, `TABLE`) with the parameters as `:NAME` bind values, plus `:APP_USER` and items | as the application's database role (grants and row level security apply), in a **read-only transaction** (functions it calls can't write either), with a time limit (1–60 s, default 15) and at most *maximum rows* rows (default 50) back to the model |
1410
+ | **PL/pgSQL function** | `schema.function` called with named arguments: each parameter name in lower case is an argument name (`P_EMPNO` → `p_empno => …`) | as the application's role; it **may change data** (committed); same time limit and rows |
1411
+ | **REST data source** | a REST data source of the application; the parameters become its parameters | as in [assistant regions](04-pages-and-regions.md#ai_assistant-ai-assistant) |
1412
+ | **JavaScript** | nothing on the server: after the answer the browser calls the function registered with `pgkiln.actions.register(action)` (with `{value: arguments}`) and the dynamic actions of the **custom event** of that name (the arguments as JSON) | once, right after the answer; the arguments are escaped data in the page |
1413
+ | **Navigate** | the model picks one of the tool's **pages**, a link text and item values of that page; pgkiln builds the URL with the item values' **checksum** (bound to application, page and user) and shows the link under the answer | only pages the user may open are offered (the page's authorization scheme and sign-in); only items of that page; the model's values are never substituted (`&ITEM.`); nothing opens the link by itself |
1414
+
1415
+ The model's arguments are checked against the parameters before a tool runs; they are bind values
1416
+ (escaped literals), never SQL text, and can't replace the server's own names (`:APP_USER`, `:APP_ID`, …).
1417
+ Tool results go back to the model as data, with the instruction never to follow instructions inside them.
1418
+
1419
+ **Every tool call is logged** with the arguments the model gave, its status (*ok*, *error*, or *refused*
1420
+ for a page the user may not open) and duration: in the page's debug messages (level 4) and under
1421
+ Activity → **AI usage** → *Tool calls* (`meta.ai_tool_call`, kept 30 days; arguments may hold user data,
1422
+ only developers see them). The token usage of each call to the model is logged as before.
1423
+
1424
+ Agents and tools are kept in the application (`meta.app.ai_agents`, `meta.app.ai_tools`) and **exported
1425
+ with it**; an export made before they existed imports with none. The forms check each one before
1426
+ saving (a SQL tool must be a query, a function `schema.name`, a navigate tool's pages must exist); the
1427
+ runtime checks them again and shows *This assistant is not set up yet* for a broken one. Renaming a
1428
+ tool keeps it in its agents; a tool an agent uses can't be deleted; renaming an agent renames it in
1429
+ the regions that use it.
1430
+
1431
+ The HR example (`examples/hr/hr_64_ai_agent.sql`) has an agent `HR_AGENT` on page 46: ask *Who in Sales
1432
+ earns most?* and it calls the SQL tool `top_earners` as the signed-in user's role, then offers
1433
+ *Open BLAKE's record*, a link to the employee form (page 3) with a valid checksum, only to users who
1434
+ may open that page.
1435
+
1322
1436
  ## App Builder AI
1323
1437
 
1324
1438
  The App Builder uses one AI service (APEX: AI Assistant in App Builder and SQL Workshop). An
@@ -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
 
@@ -1297,8 +1342,12 @@ forms *Departments* and *Book a meeting*.
1297
1342
  ### `ai_assistant` (AI assistant)
1298
1343
 
1299
1344
  An **AI assistant** region (APEX: AI Assistant, AI agents and tools) is a chat with an
1300
- [AI service](06-processing.md#generate-text-with-ai) (Claude or OpenAI) that an administrator
1301
- 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,
1302
1351
  **context queries** and **tools**; the model then answers questions about the application's data,
1303
1352
  calling the tools when it needs to, like an agent. It works without JavaScript (the message is
1304
1353
  posted and the page comes back with the answer); with JavaScript the Send button shows that the
@@ -1344,6 +1393,7 @@ Attributes (written by the region's **Settings**; they travel with the applicati
1344
1393
 
1345
1394
  | Key | Default | Meaning |
1346
1395
  |---|---|---|
1396
+ | `agent` | | (121) An [AI agent](03-builder.md#ai-agents) of the application; then leave out `service`, `system`, `tools` and `max_rounds` |
1347
1397
  | `service` | | The AI service's name |
1348
1398
  | `system` | | The system prompt (`&ITEM.` substitutions as delimited data) |
1349
1399
  | `welcome` | | The first message shown (translatable) |
@@ -1357,7 +1407,7 @@ Attributes (written by the region's **Settings**; they travel with the applicati
1357
1407
 
1358
1408
  The HR example's page 38 (*HR assistant*) has an assistant with a context query over `hr.policy`
1359
1409
  and four tools (the user's own leave, colleagues, departments, and *request_leave*, which files a
1360
- 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).
1361
1411
 
1362
1412
  ### Template components
1363
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.