@xeplr/workflow 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +352 -0
  3. package/bin/www +7 -0
  4. package/db/xcfgSetup.js +8 -0
  5. package/env.required.js +41 -0
  6. package/index.js +313 -0
  7. package/lib/actionCatalog.js +193 -0
  8. package/lib/actions/jobRun.js +149 -0
  9. package/lib/actions/screenShow.js +55 -0
  10. package/lib/db.js +82 -0
  11. package/lib/envExposed.js +87 -0
  12. package/lib/flows.js +832 -0
  13. package/lib/flowsRouter.js +113 -0
  14. package/lib/router.js +260 -0
  15. package/lib/workflowRunner.js +649 -0
  16. package/migrations/0001_companies.sql +23 -0
  17. package/migrations/0002_workspaces.sql +22 -0
  18. package/migrations/0003_workflows.sql +35 -0
  19. package/migrations/0004_workflow_steps.sql +79 -0
  20. package/migrations/0005_workflow_runs.sql +49 -0
  21. package/migrations/0006_workflow_step_runs.sql +42 -0
  22. package/migrations/0007_workflow_resume_keys.sql +42 -0
  23. package/migrations/0008_workflow_run_edges.sql +43 -0
  24. package/migrations/0009_workflow_steps_layout.sql +11 -0
  25. package/migrations/0010_workflow_steps_sample_output.sql +17 -0
  26. package/migrations/0011_workflow_steps_params.sql +27 -0
  27. package/migrations/0012_workflows_kind.sql +27 -0
  28. package/migrations/0013_workflows_key.sql +48 -0
  29. package/migrations-auth/0001_workflow_access.sql +129 -0
  30. package/migrations-auth/0003_nav_menus.sql +62 -0
  31. package/migrations-auth/0004_flows_access.sql +76 -0
  32. package/models/Company.js +82 -0
  33. package/models/Workflow.js +63 -0
  34. package/models/WorkflowResumeKey.js +32 -0
  35. package/models/WorkflowRun.js +64 -0
  36. package/models/WorkflowRunEdge.js +49 -0
  37. package/models/WorkflowStep.js +66 -0
  38. package/models/WorkflowStepRun.js +49 -0
  39. package/models/Workspace.js +75 -0
  40. package/models/index.js +25 -0
  41. package/orchestration/standalone.js +123 -0
  42. package/package.json +69 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xeplr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,352 @@
1
+ # @xeplr/workflow
2
+
3
+ **Workflows over [`@xeplr/actions`](https://www.npmjs.com/package/@xeplr/actions).** A workflow is a sequence of steps. Each step names a registered action, binds its inputs to what earlier steps produced, and routes to the next — branching on conditions, fanning out over lists, and waiting for a person or a job before it carries on. This package is the workflow document (a workflow and its steps), the engine that runs it, and the HTTP router the builder talks to. The screens are [`@xeplr/ui-workflow`](https://www.npmjs.com/package/@xeplr/ui-workflow).
4
+
5
+ It runs two ways: **embedded**, where a host calls `registerWorkflow()` and mounts the returned router (Xeplr BI does this at `/workflow`, with its own auth and its own `DB_WORKFLOW` database), or **standalone** via `npm run start-api` / the `xeplr-workflow-server` command, which is the same `registerWorkflow()` call with its settings read from `development.env`.
6
+
7
+ | what | how |
8
+ |---|---|
9
+ | A step is an action | Each step names an action registered in `@xeplr/actions`; its form comes from that action's own input schema (`GET /actions`), not a second description of it. Inputs bind to earlier steps' output, the run's parameters, or a setting on an allowlist. |
10
+ | Branch, fan out, join | Conditions are compiled with the same `@xeplr/expression-handler` the engine runs. An *each* transition starts one child run per item, and a join step picks the parent back up once every child has finished. |
11
+ | Wait for a person or a job | A wait step parks the run and issues a single-use resume link — followed from an email or called back by a job — that cannot be claimed until the step is actually waiting. |
12
+ | Screens as steps (flows) | An app's forms, one after another, with arrows that test one field — the `/flows` facade (`lib/flows.js`). |
13
+ | Parameters checked first | A run's parameters are the union of what every step declares, validated before the run is recorded. |
14
+
15
+ What it does not do yet: a step's timeout is saved but not enforced; a draft workflow can still be run; past runs are recorded but there is no screen to browse them.
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ npm i @xeplr/workflow express
21
+ ```
22
+
23
+ MIT. Its dependencies are the other `@xeplr/*` packages it builds on — actions, auth, base-apis, db, email, expression-handler, schema-handler, utils.
24
+
25
+ **One repo, two packages.** [`Xeplr/x-flow`](https://github.com/Xeplr/x-flow) holds this package at its root and `@xeplr/ui-workflow` in [`ui/`](ui). Each is installed, tested and released on its own: CI runs once per folder, and a push releases only the package whose files changed — tags `workflow-v<version>` here, `ui-workflow-v<version>` for the screens (the `xeplr` field in each `package.json`).
26
+
27
+ ## Run it
28
+
29
+ ### Standalone
30
+
31
+ ```bash
32
+ npm install
33
+ npm run check-env # fails naming any missing required var
34
+ npm run db:create # creates the DB_API database
35
+ npm run start-api # NODE_ENV=development node ./bin/www — listens on WORKFLOW_PORT
36
+ ```
37
+
38
+ `start-api` runs this package's migrations on boot, so `migrate:up` is only
39
+ needed to migrate without starting the server.
40
+
41
+ | Script | What it does |
42
+ |---|---|
43
+ | `start-api` | `NODE_ENV=development node ./bin/www` → `orchestration/standalone.js` |
44
+ | `check-env` | `xeplr-check-env` against `development.env` |
45
+ | `db:create` | `xeplr-migrate create-db` for connection `api` (`WORKFLOW_CONNECTION`) |
46
+ | `migrate:up` / `migrate:status` | `xeplr-migrate` over `./migrations` |
47
+ | `db:encrypt` | `xeplr-db-encrypt` — produce an encrypted connection string |
48
+ | `test` | `node test/run.mjs` |
49
+
50
+ Port in `development.env`: **19122** (`WORKFLOW_PORT`). No port literal exists
51
+ in code; a missing `WORKFLOW_PORT` stops the process at `checkEnv`.
52
+
53
+ Required services for standalone:
54
+
55
+ | Service | Why |
56
+ |---|---|
57
+ | PostgreSQL | `DB_API` (this package's tables), `XCFG_DB_NAME` (shared `xeplr_configs`), `EMAIL_DB_NAME` (template store) |
58
+ | An `@xeplr/auth` server + its database | `@xeplr/auth` `attach()` reads users/tenant grants from `AUTH_DB_NAME`; the createApp gate validates tokens against `AUTH_URL`. This package has **no** `start-auth` script — the auth server is run separately (`xeplr-auth-server`), and it applies `migrations-auth/` via `XEPLR_AUTH_MIGRATIONS` |
59
+
60
+ Boot order (`orchestration/standalone.js`): load `<NODE_ENV>.env` → `checkEnv`
61
+ → `registerApplication(WORKFLOW_APPLICATION_ID || 'xeplr-workflow')` → run
62
+ migrations → `xeplr_configs` ready (**fail-fast**) → register action catalog →
63
+ `@xeplr/auth` attach + email config → email templates (**best-effort**) →
64
+ `registerWorkflow({ port, mountPath: '/', auth: { publicPaths: ['/public/', '/events'] }, ... })`.
65
+
66
+ ### Embedded (how Xeplr BI does it)
67
+
68
+ ```js
69
+ // host env.required.js
70
+ ...require('@xeplr/workflow').embedRequiredEnv, // ['DB_WORKFLOW']
71
+
72
+ // host startup, after the host's own registerApplication() and registerMTs()
73
+ var { registerWorkflow } = require('@xeplr/workflow');
74
+ var { router } = await registerWorkflow({
75
+ db: { name: process.env.DB_WORKFLOW, connection: hostConnection, connectionName: 'workflow' },
76
+ mtMembershipGate: hostGate,
77
+ authMiddleware: require('@xeplr/auth').authMiddleware
78
+ });
79
+ routes['/workflow'] = router; // into createApp's route map, not app.use() afterwards
80
+ ```
81
+
82
+ Embedded, `registerWorkflow` creates the database if missing, runs this
83
+ package's migrations (plus `XEPLR_WORKFLOW_MIGRATIONS` or `config.migrations`),
84
+ tries `xeplr_configs` (warns and continues on failure), registers the action
85
+ catalog, and returns the router.
86
+
87
+ ### Environment
88
+
89
+ Names and meaning only. Values live in `development.env` — copy
90
+ `development.env.example`, which has every name and no values. It is
91
+ git-ignored (`.gitignore`) because it carries secrets. Never commit it.
92
+
93
+ **Standalone — `env.required.js` (checked at boot):**
94
+
95
+ | Variable | Meaning | Comes from |
96
+ |---|---|---|
97
+ | `AUTH_URL` | Auth server the createApp gate validates tokens against | `@xeplr/base-apis` `gateRequiredEnv` |
98
+ | `ENCRYPTION_KEY`, `AUTH_JWT_SECRET`, `AUTH_PORT`, `AUTH_DB_NAME`, `XEPLR_AUTH_MIGRATIONS` | Decrypts stored connection strings; auth DB and token settings | `@xeplr/auth` `requiredEnv` (the inline comment in `env.required.js` lists older names) |
99
+ | `EMAIL_PROVIDER`, `BREVO_*` | Outbound mail provider | `@xeplr/email` `requiredEnv` |
100
+ | `EMAIL_DB_NAME` | This app's own email template store | `@xeplr/email` `templatesRequiredEnv` |
101
+ | `XCFG_DB_NAME`, `XCFG_DB_CONNECTION_INFO_ENCRYPTED` | Shared `xeplr_configs` control-plane DB | `@xeplr/actions` `configRequiredEnv` |
102
+ | `DB_API` | This package's database name | app |
103
+ | `WORKFLOW_PORT` | API port | app |
104
+ | `AUTH_SUPER_ADMIN_PASSWORD` | First account's password, consumed by auth's super-admin migration | app |
105
+
106
+ **Read but not in the required list:**
107
+
108
+ | Variable | Meaning |
109
+ |---|---|
110
+ | `WORKFLOW_CONNECTION` | Override for the server login. Normally the shared `XEPLR_DB_CONNECTION` is used; `resolveDbConnection` names both if neither is set |
111
+ | `XEPLR_DB_CONNECTION` | Shared server login every xeplr service reads |
112
+ | `WORKFLOW_APPLICATION_ID` | Application id for rows written to `xeplr_configs`; defaults to `xeplr-workflow` standalone |
113
+ | `LOG_DIR` | Log directory; `./logs` if unset |
114
+ | `NODE_ENV` | Selects `<NODE_ENV>.env` |
115
+ | `WORKFLOW_ENV_EXPOSED` | Comma-separated **exact** env var names step templates may read as `{env.NAME}`. Empty = nothing readable |
116
+ | `WORKFLOW_PUBLIC_URL` | Base URL callers reach this service on, mount path included. Builds `{resumeUrl}`. Exported as `callbackRequiredEnv`; only needed for steps that call out (today `job-run`) |
117
+ | `JOBS_API_URL` | Where the server reaches the jobs API; the jobs canvas binds `job-run.jobsUrl` to `{env.JOBS_API_URL}`, so it must also be named in `WORKFLOW_ENV_EXPOSED` |
118
+ | `XEPLR_WORKFLOW_MIGRATIONS` | Extra migration directories a host adds to workflow's database |
119
+ | `AUTH_DB_CONNECTION_INFO_ENCRYPTED`, `AUTH_ACTIVATION_BASE_URL` (or `AUTH_ACTIVATION_URL`), `AUTH_ACCESS_TOKEN_TTL_MINUTES`, `AUTH_ACCESS_TOKEN_TOLERANCE_SECONDS`, `AUTH_SUPER_ADMIN_EMAIL` | Read by `@xeplr/auth` / `xeplr-auth-server` |
120
+ | `BREVO_API_KEY`, `BREVO_FROM_EMAIL`, `BREVO_FROM_NAME`, `EMAIL_TEST_TO` | Read by `@xeplr/email` |
121
+ | `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`, `REDIS_DB` | Redis (commented in `development.env`: defaults `localhost:6379`) |
122
+
123
+ **Embedded — `embedRequiredEnv`:** `DB_WORKFLOW` only. The host passes the
124
+ database name and connection into `registerWorkflow({ db })`; this package
125
+ never reads either variable itself.
126
+
127
+ ## Structure
128
+
129
+ ```
130
+ xeplr-workflow/
131
+ ├─ index.js registerWorkflow, resumeByKey, embedRequiredEnv, callbackRequiredEnv
132
+ ├─ bin/www process entry → orchestration/standalone.js
133
+ ├─ orchestration/
134
+ │ └─ standalone.js the ONLY file that reads env for config
135
+ ├─ env.required.js standalone required-env list
136
+ ├─ lib/
137
+ │ ├─ router.js buildWorkflowRouter(config) — all HTTP routes
138
+ │ ├─ workflowRunner.js the engine: startRun, resumeByKey, resolveParams, collectParams
139
+ │ ├─ actionCatalog.js whitelist of @xeplr/actions built-ins + local actions
140
+ │ ├─ actions/jobRun.js local `job-run` action (start a job, park until callback)
141
+ │ ├─ envExposed.js WORKFLOW_ENV_EXPOSED allowlist for {env.*}
142
+ │ └─ db.js connectDb + model(name) bound to workflow's own connection
143
+ ├─ db/xcfgSetup.js shared @xeplr/actions attachConfig({ service: 'xeplr-workflow' })
144
+ ├─ models/ Company, Workspace, Workflow, WorkflowStep, WorkflowRun,
145
+ │ WorkflowStepRun, WorkflowResumeKey, WorkflowRunEdge
146
+ ├─ migrations/ 0001–0012, this package's own database
147
+ ├─ migrations-auth/ rows for the AUTH database (roles, menus, apis)
148
+ └─ test/ run.mjs + *.test.mjs
149
+ ```
150
+
151
+ ## API
152
+
153
+ ### Exports (`index.js`)
154
+
155
+ | Export | Purpose |
156
+ |---|---|
157
+ | `registerWorkflow(config)` | Connect, create DB if missing, migrate, register actions, build router. Returns `{ router }` (no `app`, no `port`), `{ router, app }` (with `config.app`, mounted at `mountPath`, default `/workflow`), or `{ router, app, server }` (with `config.port`, via `@xeplr/base-apis` `createApp`). `config.db` is always required. Throws if no application id is registered |
158
+ | `resumeByKey(key, output?, opts?)` | Server-side, in-process resume of a run parked on a `wait` step. `opts.status: 'failed'` resolves the step as failed. Throws `err.code` `RESUME_KEY_INVALID` (used/unknown — normal for host routes) or `RESUME_KEY_NOT_READY` (step still running; nothing consumed, retry). Requires `registerWorkflow()` to have run in the process |
159
+ | `embedRequiredEnv` | `['DB_WORKFLOW']` — spread into a host's `env.required.js` |
160
+ | `callbackRequiredEnv` | `['WORKFLOW_PUBLIC_URL']` — only for hosts using steps that call out |
161
+
162
+ `registerWorkflow` config: `app`, `db` (`{ name, connection, connectionName?, mts? }`),
163
+ `applicationId`, `mountPath`, `port`, `appName` (default `xeplr_workflow_api`),
164
+ `mtMembershipGate`, `authMiddleware`, `middleware`, `log`, `auth`, `migrations`.
165
+ Pass `db.mts` only when nothing else in the process has called `registerMTs()`
166
+ (it is process-global).
167
+
168
+ ### Routes (`lib/router.js`)
169
+
170
+ Paths are relative to the mount (`/` standalone, `/workflow` in xeplr-bi).
171
+ "Gated" = `config.mtMembershipGate` (a no-op if not supplied).
172
+
173
+ | Method | Path | Gated | Purpose |
174
+ |---|---|---|---|
175
+ | GET | `/` | no | Health: `{ service, status, name }` |
176
+ | GET | `/me` | via `authMiddleware` | Current user; only mounted when `authMiddleware` is passed |
177
+ | GET | `/actions` | yes | Action catalog: `name`, `description`, `inputSchema`, `outputSchema` (never the executor) |
178
+ | GET | `/companies`, `/companies/:id` | **no** | Company `genericRoute` (Company is `multiTenant = false`) |
179
+ | POST | `/companies/save`, `/companies/delete` | **no** | " |
180
+ | GET | `/workspaces`, `/workspaces/:id` | yes | Workspace `genericRoute` |
181
+ | POST | `/workspaces/save`, `/workspaces/delete` | yes | " |
182
+ | GET | `/workflows`, `/workflows/:id` | yes | Workflow list/get with `steps` eager-loaded |
183
+ | POST | `/workflows/save` | yes | Save a workflow and its whole step list in one transaction (changeset) |
184
+ | POST | `/workflows/delete` | yes | `{ ids: [...] }` |
185
+ | POST | `/workflows/:id/run` | yes | `startRun(id, body)`. Runs synchronously to the first wait step or the end; 400 carries `{ message, code, details }` |
186
+ | GET | `/workflows/:id/steps/:stepKey/last-output` | yes | Output of the latest `success`/`waiting` step run for that key. No side effects |
187
+ | POST | `/workflows/:id/steps/try` | yes | **Runs the action for real** with `{ actionName, stepKey, values, params }`, interpolated against other steps' `sampleOutput`. Echoes the resolved `input` |
188
+ | POST | `/public/resume/:key` | **no** | Resume a wait step; body `{ output?, status?, error? }`. The host must also exempt `<mount>/public/*` from its own auth |
189
+ | GET | `/email-templates`, `/email-templates/:name` | yes | From `@xeplr/email` `templatesRouter`; mounted only if templates are initialised |
190
+ | POST | `/email-templates/save`, `/email-templates/delete`, `/email-templates/:name/preview` | yes | " |
191
+
192
+ ### Flows — screens as steps (`lib/flows.js`, `lib/flowsRouter.js`)
193
+
194
+ A **flow** is a workflow of kind `screens`: every step shows a screen (a
195
+ `@xeplr/ui-factory` form) to a person and waits for them to submit it, and the
196
+ transitions leaving it decide which screen comes next. The engine does not know
197
+ the difference — a screen step is an ordinary `wait` step whose action is
198
+ `screen-show`, and the submitted values are that step's `output`, so a
199
+ transition reads `output.<field>` exactly as it does anywhere else.
200
+
201
+ A flow is **designed in the app that owns the screens** (Configure UI → Flows),
202
+ through this facade, and addressed by a `key` that app chooses. The workflow
203
+ routes refuse to create or save one — *"This flow is designed in Configure UI"*
204
+ — and the builder shows it read-only. Runs and history are visible as usual.
205
+
206
+ Mounted at `<mount>/flows` by `registerWorkflow`, and returned on its own as
207
+ `flowsRouter` for a host that mounts it elsewhere. Every route is gated.
208
+
209
+ | Method | Path | Right | Body → answer |
210
+ |---|---|---|---|
211
+ | GET | `/flows` | view | → `[{ id, key, name, status, steps }]` (`steps` is a count) |
212
+ | POST | `/flows` | create | `{ key, name }` → the new draft flow |
213
+ | GET | `/flows/:key` | view | → `{ id, key, name, status, steps: [{ stepKey, label, screen, layout, transitions }] }` |
214
+ | PUT | `/flows/:key` | create | `{ name?, steps }` → the flow. Drafts only |
215
+ | POST | `/flows/:key/publish` | create | → the flow, once it checks out (below) |
216
+ | POST | `/flows/:key/runs` | run | → `{ runId, status, stepKey, screen }` |
217
+ | GET | `/flows/runs/:runId` | view | → `{ runId, status, stepKey, screen, values }` — `values` is what that step already holds, so a resumed run reopens filled in |
218
+ | POST | `/flows/runs/:runId/submit` | run | `{ values, recordId? }` → the next `{ status, stepKey, screen }`, or `{ status: 'done' }` |
219
+ | GET | `/flows/:key/runs?mine=1` | view | → runs still going, newest first |
220
+
221
+ **A transition is structured, never a formula**, in and out:
222
+
223
+ ```json
224
+ { "when": { "field": "type", "op": "=", "value": "contractor" }, "target": "contract" }
225
+ { "when": null, "target": "payroll" }
226
+ ```
227
+
228
+ `op` is one of `= != > >= < <= contains notContains startsWith endsWith in notIn
229
+ between isEmpty isNotEmpty` (the engine's own names, `eq`/`gt`…, are accepted on
230
+ the way in); `in`, `notIn` and `between` take a list. A `target` is a step key,
231
+ `end_success` or `end_failed`.
232
+
233
+ **Publishing checks** that every step names a screen, every target exists, a
234
+ step has at most one catch-all and it comes last, every step leads somewhere
235
+ or ends, and every step is reachable from the first.
236
+
237
+ **The browser never holds a resume key.** Submit is an authenticated call; the
238
+ facade finds the waiting step's key itself and resumes through `resumeByKey`.
239
+ `recordId` — the row the screen wrote into its own table — is kept on the step's
240
+ output, so later steps and their conditions can use it.
241
+
242
+ **A run belongs to the company it was started in.** Another company's flow is
243
+ not there, and its run ids are 404, not 403 — nothing confirms they exist.
244
+
245
+ Rights come from `migrations-auth/0004_flows_access.sql`: submitting is a
246
+ **run** right, not a view right, because it moves the run on and everything
247
+ after it follows.
248
+
249
+ ## Data model and migrations
250
+
251
+ Two directories, two databases:
252
+
253
+ | Directory | Target DB | How it runs |
254
+ |---|---|---|
255
+ | `migrations/` | workflow's own (`DB_API` standalone, `DB_WORKFLOW` in BI) | On every boot inside `registerWorkflow` (and once more in `standalone.js`); idempotent. Also `npm run migrate:up`. Plain `.sql`, ledger-tracked by filename, `precede` order |
256
+ | `migrations-auth/` | the auth database | Not run by this package. Pointed at by `XEPLR_AUTH_MIGRATIONS`; applied by `@xeplr/auth` (`xeplr-auth-server` boot or `xeplr-auth-migrate up`) after auth's own migrations |
257
+
258
+ | Migration | Creates |
259
+ |---|---|
260
+ | `0001_companies` | `companies` (l1 tenant) |
261
+ | `0002_workspaces` | `workspaces` (l2, under a company) |
262
+ | `0003_workflows` | `workflows` — `name`, `description`, `params` (jsonb), `status` (default `draft`) |
263
+ | `0004_workflow_steps` | `workflow_steps` — `stepKey` (unique per workflow), `actionName`, `values`, `kind` (`auto`/`wait`), `timeoutMs`, `onError` (`stop`/`continue`), `transitions`, `joinStep`, `position` |
264
+ | `0005_workflow_runs` | `workflow_runs` — `status` (`queued`/`running`/`waiting`/`success`/`failed`), `params`, `item`, `trigger`, timings, `error` |
265
+ | `0006_workflow_step_runs` | `workflow_step_runs` — copied `stepKey`/`actionName`, resolved `input`, `output`, `error` |
266
+ | `0007_workflow_resume_keys` | `workflow_resume_keys` — unique random `key`, `consumedDate` |
267
+ | `0008_workflow_run_edges` | `workflow_run_edges` — parent → child run for `each` fan-out; `childRunId` unique |
268
+ | `0009` | `workflow_steps.layout` (canvas `{x, y}`) |
269
+ | `0010` | `workflow_steps.sampleOutput` |
270
+ | `0011` | `workflow_steps.params` |
271
+ | `0012` | `workflows.kind` (`workflow` \| `jobs`, default `workflow`) |
272
+ | `migrations-auth/0001_workflow_access` | Roles `Super Admin`, `CompanyAdmin`, `Creator`, `Viewer`; menus `Home`, `Actions`, `Configuration`, `Access Control`; `apis` rows; role → menu/api matrix |
273
+ | `migrations-auth/0003_nav_menus` | Menus `Dashboards`, `Jobs`, `Select Workspace`, `Select Company` (all `workflows:view`) + mappings |
274
+
275
+ Every table carries `mtId1`–`mtId4` and **no `applicationId`**: the database a
276
+ host gives workflow is the boundary.
277
+
278
+ ## Engine behaviour (`lib/workflowRunner.js`)
279
+
280
+ - **Step values** are interpolated with `@xeplr/schema-handler` against
281
+ `{ params, item, steps.<key>.output, previous_step.output, env, resumeKey, resumeUrl }`.
282
+ `previous_step`/`steps` come from what ran in this run, not array position.
283
+ - **Routing**: no `transitions` → next step by `position`. With transitions →
284
+ first match wins (`@xeplr/expression-handler`); a blank condition is the
285
+ catch-all; targets are a `stepKey`, `end_success` or `end_failed`. No match
286
+ and no catch-all **fails the run**.
287
+ - **Fan-out**: a matching `each` transition creates one child run per matching
288
+ element (`item`), recorded in `workflow_run_edges`. With `joinStep` the parent
289
+ waits and resumes at the join once all children are terminal (guarded by an
290
+ atomic `waiting → running` patch); without it the parent finishes.
291
+ - **Wait steps**: the resume key row is written **before** the action runs;
292
+ the run parks with status `waiting`. If the action fails the key is marked
293
+ consumed.
294
+ - **Action failure**: `onError: 'continue'` advances by position; otherwise the
295
+ run fails.
296
+ - **Run params**: the run schema is the union of every step's `params` (plus
297
+ legacy `workflows.params` first). Any step requiring a name makes it required;
298
+ otherwise first declaration by position wins. Validated with `applySchema`
299
+ **before** the run row is inserted; undeclared keys are dropped; failure has
300
+ `code: 'PARAMS_INVALID'` and `details`. A workflow that declares nothing
301
+ accepts params unchanged.
302
+ - **Tenant context**: after the first lookup, all DB work runs inside
303
+ `runWithMt()` with the run's stored `mtId`s, because a resume can arrive long
304
+ after the starting request.
305
+
306
+ Not read by the runner: `workflows.status`, `workflows.kind`,
307
+ `workflow_steps.layout`, `workflow_steps.sampleOutput`, `workflow_steps.timeoutMs`.
308
+
309
+ ## Rules the code enforces, and why
310
+
311
+ | Rule | Why (from the code comments) |
312
+ |---|---|
313
+ | Action catalog is a **whitelist** (`WANTED`, `LOCAL`, `WANTED_WITH_META`) | Offering a dangerous built-in should be a visible decision in a diff, not a side effect of upgrading. Placeholders in `@xeplr/actions` would appear in the picker and fail when used |
314
+ | `spawnProgram` is deliberately absent (asserted in tests) | It runs arbitrary executables |
315
+ | Placeholders (no `execute`/`name`) are skipped and logged, not thrown | The app works without them; they land when the package implements them |
316
+ | `{env.*}` reaches only names in `WORKFLOW_ENV_EXPOSED`; exact names, no globs | Resolved step input is echoed by `/steps/try` and persisted on step runs, so an unlisted env would let any step author read `ENCRYPTION_KEY` / `AUTH_JWT_SECRET`. An allowlist fails closed when someone adds a new secret |
317
+ | Never put a credential in `WORKFLOW_ENV_EXPOSED` | Secrets belong where the action reads them server-side (e.g. `useCustomConnection: false` with `SMTP_*`) |
318
+ | `WORKFLOW_PUBLIC_URL` is not defaulted | A wrong-but-present callback address makes a run wait forever on work that finished; unset, `job-run` refuses by name |
319
+ | Models are always bound via `db.model(name)`; connection uses `bind: false` | `bindModels` is global in Objection; binding workflow's connection globally re-pointed the host's models at workflow's database |
320
+ | `registerWorkflow` runs its own migrations and action registration | Previously only standalone did, so embedded hosts got missing tables and an empty catalog with no error |
321
+ | `xeplr_configs` is fail-fast standalone, best-effort embedded | Standalone owns its process and should not boot half-configured; embedded, one metadata-writing action should not take down the host's product |
322
+ | An application id must be registered | Rows written to shared `xeplr_configs` are attributed by application |
323
+ | Hosts must not mount the router at `/` | It serves `/companies`, `/workspaces`, `/actions`, which a host already serves |
324
+ | Hosts should take the router, not `app.use()` it onto a built createApp app | createApp adds a catch-all 404 after its routes, so later mounts are unreachable |
325
+ | `/steps/try` is real execution; the UI confirms first | Same `runAction` the engine uses — `email-delete` expunges, `db-push` writes |
326
+ | `/public/resume/:key` is ungated | A resume link is followed from an email client or a job callback with no token |
327
+ | `resumeByKey` refuses (`RESUME_KEY_NOT_READY`) until the step is `waiting`; claim is an atomic conditional patch | The key exists before the action finishes (mail scanners pre-fetch links); resuming mid-action would route the run on while the action is still running |
328
+ | `job-run`: HTTP 409 from the jobs API is a failure, not a retry | A chain must not continue past a movement that never ran |
329
+ | Role matrix: Creator can run, Viewer cannot | A run has side effects (mail, files, database writes) |
330
+
331
+ ## Tests
332
+
333
+ ```bash
334
+ npm test # node test/run.mjs — each *.test.mjs in its own process
335
+ node --preserve-symlinks test/runParams.test.mjs # one suite on its own
336
+ ```
337
+
338
+ Plain scripts, no framework; each prints `ok`/`FAIL` lines and exits non-zero on
339
+ failure. A suite that needs node flags declares them in a `// @flags:` header
340
+ (three use `--preserve-symlinks` so dev-linked `@xeplr/*` packages resolve their
341
+ peers from this app's `node_modules`). No database or network needed; needs
342
+ `npm install` (real `@xeplr/actions` and `@xeplr/schema-handler`).
343
+
344
+ | Suite | Covers |
345
+ |---|---|
346
+ | `actionCatalog.test.mjs` | Ready vs placeholder detection; registered names are kebab-case; `spawnProgram` not offered |
347
+ | `flows.test.mjs` | The `/flows` facade: flows as workflows, steps and arrows, runs and submits (against a fake database) |
348
+ | `envExposed.test.mjs` | Nothing exposed by default; exact, case-sensitive matching; globs fail closed (against real `interpolateAll`) |
349
+ | `runParams.test.mjs` | `resolveParams` / `collectParams`: required, defaults, types, typos, union rules |
350
+
351
+ Not covered by tests: the router, `startRun`/`resumeByKey` against a database,
352
+ migrations, `job-run`.
package/bin/www ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ // All of this process's startup sequence — env loading, migrations,
4
+ // xeplr_configs, action registration, auth wiring, and finally
5
+ // registerWorkflow() — lives in orchestration/standalone.js now. This file
6
+ // is just the process entry point.
7
+ require('../orchestration/standalone');
@@ -0,0 +1,8 @@
1
+ // Single shared instance of @xeplr/actions' attachConfig() — so bin/www (which
2
+ // calls ready()) and anything else reading control-plane metadata see the same
3
+ // connection. All the bootstrap/bind/metaStore logic lives in @xeplr/actions;
4
+ // this file is just the one place xeplr-workflow holds its instance, naming
5
+ // itself 'xeplr-workflow'.
6
+ var { attachConfig } = require('@xeplr/actions');
7
+
8
+ module.exports = attachConfig({ service: 'xeplr-workflow' });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Mandatory env vars for the xeplr-workflow API process running STANDALONE.
3
+ * Checked by orchestration/standalone.js at startup and at build (`npm run
4
+ * check-env`). The auth service (xeplr-auth-server) reads its own env directly.
5
+ *
6
+ * NOT the list for an EMBEDDED mount — see `embedRequiredEnv` in index.js. A
7
+ * host app passes workflow's connection and database name as arguments to
8
+ * registerWorkflow({ db }), so none of the connection vars below apply to it,
9
+ * and registerWorkflow never runs this check.
10
+ *
11
+ * Framework var NAMES are owned by the libraries — spread their `requiredEnv`
12
+ * so you never re-list them, and a new lib requirement lands in every app for
13
+ * free. Only APP-SPECIFIC vars are listed literally.
14
+ *
15
+ * Same shape as xeplr-bi's, deliberately: two apps that boot differently are
16
+ * two apps to learn.
17
+ */
18
+ module.exports = [
19
+ ...require('@xeplr/base-apis').gateRequiredEnv, // AUTH_URL — standalone uses createApp's own gate
20
+ ...require('@xeplr/auth').requiredEnv, // ENCRYPTION_KEY, JWT_SECRET, ACTIVATION_BASE_URL
21
+ ...require('@xeplr/email').requiredEnv, // EMAIL_PROVIDER, BREVO_* (app sends invite mail)
22
+ // This app also OWNS A TEMPLATE STORE (orchestration/standalone.js calls
23
+ // initTemplates). Templates are app-specific — a registration mail is
24
+ // written for one product — so the store is this app's own, not a shared
25
+ // xeplr_email that another product could overwrite by template name.
26
+ ...require('@xeplr/email').templatesRequiredEnv, // EMAIL_DB_NAME
27
+ ...require('@xeplr/actions').configRequiredEnv, // XCFG_DB_NAME, XCFG_DB_CONNECTION_INFO_ENCRYPTED
28
+
29
+ // App-specific — names this app chose (checked by the API process):
30
+ //
31
+ // WORKFLOW_CONNECTION is NOT here, deliberately: it is an OVERRIDE. The
32
+ // server login normally comes from the shared XEPLR_DB_CONNECTION that every
33
+ // xeplr service reads, so demanding the workflow-specific name would fail a
34
+ // correctly configured install. It still wins when set. Missing-ness is
35
+ // caught at the point of use by resolveDbConnection (see standalone.js),
36
+ // which names both variables. Same call auth and actions already made —
37
+ // xeplr-auth/index.js and @xeplr/actions' attach.js.
38
+ 'DB_API', // api database name
39
+ 'WORKFLOW_PORT', // api port — NOT hardcoded anywhere
40
+ 'AUTH_SUPER_ADMIN_PASSWORD' // consumed by migrations-auth/0002_super_admin.sql
41
+ ];