@protobase/cli 0.67.10 → 0.67.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/shell-chunks.js +22 -11
- package/dist/cli/package.js +1 -1
- package/package.json +7 -7
- package/skills/protobase/references/features/record-pages.md +1 -0
- package/skills/protobase/references/features/workflows.md +5 -2
- package/skills/protobase/references/reference/custom-components.md +2 -2
- package/skills/protobase/references/reference/mcp.md +2 -2
- package/skills/protobase/references/reference/plugins.md +10 -0
- package/skills/protobase/references/reference/workflows.md +49 -8
|
@@ -19,19 +19,30 @@ const isVendorModule = (id) => {
|
|
|
19
19
|
const name = npmPackage(id);
|
|
20
20
|
return name !== void 0 && !reactPackages.has(name) && name !== "lucide-react";
|
|
21
21
|
};
|
|
22
|
+
const preloadHelper = "\0vite/preload-helper.js";
|
|
23
|
+
/** The chunk name of a third-party module only lazy code loads: its package's, such as `lib-xyflow-react`. */
|
|
24
|
+
const libChunkName = (id) => `lib-${npmPackage(id).replace("@", "").replace("/", "-")}`;
|
|
22
25
|
/**
|
|
23
26
|
* The shell build's `output.codeSplitting`. `react` comes first: without it, `vendor` would take React in as a
|
|
24
27
|
* dependency of the libraries it holds, and lazy chunks would import it from there.
|
|
25
28
|
*/
|
|
26
|
-
const shellCodeSplitting = { groups: [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
const shellCodeSplitting = { groups: [
|
|
30
|
+
{
|
|
31
|
+
name: "react",
|
|
32
|
+
test: isReactModule,
|
|
33
|
+
priority: 2
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "vendor",
|
|
37
|
+
test: (id) => isVendorModule(id) || id === preloadHelper,
|
|
38
|
+
tags: ["$initial"],
|
|
39
|
+
priority: 1
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: libChunkName,
|
|
43
|
+
debugName: "lib",
|
|
44
|
+
test: isVendorModule
|
|
45
|
+
}
|
|
46
|
+
] };
|
|
36
47
|
//#endregion
|
|
37
|
-
export { isReactModule, isVendorModule, npmPackage, shellCodeSplitting };
|
|
48
|
+
export { isReactModule, isVendorModule, libChunkName, npmPackage, shellCodeSplitting };
|
package/dist/cli/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.67.
|
|
3
|
+
"version": "0.67.12",
|
|
4
4
|
"description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@better-auth/core": "^1.7.12",
|
|
45
45
|
"@hono/node-server": "^2.1.3",
|
|
46
|
-
"@protobase/config": "0.67.
|
|
47
|
-
"@protobase/layout": "0.67.
|
|
48
|
-
"@protobase/query": "0.67.
|
|
49
|
-
"@protobase/schema": "0.67.
|
|
50
|
-
"@protobase/server": "0.67.
|
|
51
|
-
"@protobase/ui": "0.67.
|
|
46
|
+
"@protobase/config": "0.67.12",
|
|
47
|
+
"@protobase/layout": "0.67.12",
|
|
48
|
+
"@protobase/query": "0.67.12",
|
|
49
|
+
"@protobase/schema": "0.67.12",
|
|
50
|
+
"@protobase/server": "0.67.12",
|
|
51
|
+
"@protobase/ui": "0.67.12",
|
|
52
52
|
"@react-email/render": "^2.1.0",
|
|
53
53
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
54
54
|
"@sentry/vite-plugin": "^11.10.0",
|
|
@@ -11,6 +11,7 @@ The page of one record, with its sections, related records, galleries, charts an
|
|
|
11
11
|
- [Unsaved edits](../reference/ui-config.md#editing-a-record): Unsaved changes to a record stay in the browser and can be resumed, and leaving a form, a dialog or a comment with changes asks first.
|
|
12
12
|
- [Record forms](../reference/ui-config.md#editing-a-record): Relations are picked from a list that narrows as you type, numbers are read as the browser's language writes them, and a refused save says under each field what is wrong.
|
|
13
13
|
- [Titles per reader](../reference/ui-config.md#titles-per-reader): A record without a name of its own, such as a direct message, is titled for each reader from its fields, such as after the other members, in the sidebar, breadcrumbs, heading, browser tab, relation links, list columns, relation pickers, filters and search.
|
|
14
|
+
- [JSON fields on forms](../reference/plugins.md#json) (beta): A JSON field, such as a workflow run's input, is edited on forms as JSON text and shown written out on several lines.
|
|
14
15
|
- [Member fields](../reference/plugins.md#members): A field that holds a list of people, such as a private channel's members, picks the organization's members by name on forms and shows them by avatar and name.
|
|
15
16
|
- [Save feedback](../reference/ui-config.md#the-record-page): A view shows saving on the Save button instead of in a toast.
|
|
16
17
|
- [Save conflicts](../reference/ui-config.md#editing-a-record): When someone else changed a record while you edited it, Save shows their value beside yours for each field you both changed, and you keep yours or take theirs.
|
|
@@ -6,12 +6,15 @@ Graphs of steps the server runs along their edges, written in a language a model
|
|
|
6
6
|
|
|
7
7
|
- [Workflows](../reference/workflows.md#turning-them-on) (beta): Workflows kept in the app's database, each changed definition checked and kept as a version, run on the app's own jobs.
|
|
8
8
|
- [Workflow language](../reference/workflows.md#the-document) (beta): The States Language with JSONata expressions, every value read by name, so a model writes and edits it as a small, local change.
|
|
9
|
-
- [Checked on save](../reference/workflows.md#reading-values-by-name) (beta): A definition that does not fit the language, has an edge to no step
|
|
9
|
+
- [Checked on save](../reference/workflows.md#reading-values-by-name) (beta): A definition that does not fit the language, has an edge to no step, reads a value nothing gives or leaves out an argument its step type needs is refused, naming the step and the field.
|
|
10
|
+
- [Workflow editor](../reference/workflows.md#the-editor) (beta): The admin app draws a workflow as a graph that lays itself out, to add, connect and remove steps and fill in each step's form, with every problem the save would refuse shown on its step as it is edited.
|
|
10
11
|
- [Branches, steps per item and retries](../reference/workflows.md#states) (beta): Choices, a step per item with their results merged, waits, and per-step retries, timeouts and error paths.
|
|
11
12
|
- [HTTP request step](../reference/workflows.md#apphttp) (beta): Calls a URL built at run time through the outgoing-address guard, only at public addresses unless the config allows private ones, reading at most 10 MiB of the answer as JSON, text or a file.
|
|
12
13
|
- [Records steps](../reference/workflows.md#apprecords) (beta): Create, update, get and list records as the workflow's last editor, with the roles they hold now.
|
|
13
14
|
- [Mail step](../reference/workflows.md#appmailsend) (beta): Sends mail, with files the run downloaded attached, from the app's own sender.
|
|
14
15
|
- [Runs and step records](../reference/workflows.md#runs-and-their-steps) (beta): Each run started by hand through the API or from the app's code, with every step's input, output, logs, timing, tries and error recorded, secrets blanked out.
|
|
16
|
+
- [Run debugger](../reference/workflows.md#following-and-debugging-a-run) (beta): Each workflow lists its runs with their status, who started them, when and for how long; a run's page marks every step's status on the workflow's graph, live while it runs, with each step's input, output, logs, timing, tries and error, secrets blanked out.
|
|
17
|
+
- [Re-run from the failed step](../reference/workflows.md#re-running-a-failed-run) (beta): Once the workflow is fixed, a failed run runs again from the step that failed, keeping what the steps before it gave; a test run starts the workflow with a sample input from its page.
|
|
15
18
|
- [Runs that survive restarts](../reference/workflows.md#how-a-run-is-run) (beta): A run takes up where it stood after a restart or a crashed worker, never running a saved step again, and every limit fails closed.
|
|
16
19
|
- [Who may run workflows](../reference/workflows.md#who-may-do-what) (beta): Making, changing and running workflows and reading their runs are each granted by name, within the tenant.
|
|
17
|
-
- [Workflows over MCP](../reference/mcp.md#workflows) (beta): AI clients find, write, validate, change by a small edit and test-run workflows as the signed-in person, and read each step of a run to fix the one that failed.
|
|
20
|
+
- [Workflows over MCP](../reference/mcp.md#workflows) (beta): AI clients find, write, validate, change by a small edit and test-run workflows as the signed-in person, and read each step of a run to fix the one that failed and re-run it from there.
|
|
@@ -233,8 +233,8 @@ A component that asks for input looks like the app's own forms when it is built
|
|
|
233
233
|
| --- | --- |
|
|
234
234
|
| `Form` | The create form's frame: `submit`, the button's label; `onSubmit`; `saving` and `disabled` for the button; `onCancel`, which adds Cancel; and `error` with `errorTitle`, a failure that belongs to no field, where an `ApiError` shows its messages. With `title` (and `eyebrow`, the line above it) the form is the whole page, its buttons beside the heading, as the create form is. Without, it sits in a composed page under the page's heading, its buttons after the sections. |
|
|
235
235
|
| `Section` | A titled part of a form or page: `title`, `help`, `actions`. Sections in a `Form` have a line between them. |
|
|
236
|
-
| `Field` | A label over its input: `label`, `required`, `help`, `example`, `error`, `name` for the browser's autofill, and `wide` for both columns of a grid of two. An `Input` or `
|
|
237
|
-
| `Input`, `Textarea` | A text box, with `leading` and `trailing` content and `invalid`; a box of several lines that grows with its text
|
|
236
|
+
| `Field` | A label over its input: `label`, `required`, `help`, `example`, `error`, `name` for the browser's autofill, and `wide` for both columns of a grid of two. An `Input`, `Textarea` or `Select` inside takes its label, name, help and error from it, and whether it is invalid or required; another control spreads `useFieldControl()` on its element. |
|
|
237
|
+
| `Input`, `Textarea`, `Select` | A text box, with `leading` and `trailing` content and `invalid`; a box of several lines that grows with its text; a native select with `invalid`. |
|
|
238
238
|
| `Choices` | One of a few options, each a card in a row of its own with a `description`: `name`, `label`, `options`, `value`, `onChange`. A `disabled` option shows and cannot be chosen. |
|
|
239
239
|
| `RecordPicker` | Records of another resource, picked as the create form picks a relation: type to narrow them by their search fields or title, scroll for more, or use the keyboard. `resource` names the resource, whose model it reads from the app; `value` is the chosen record's key as text and `onChange` gets the new one. With `multiple`, `value` and `onChange` hold several keys, such as the objectives a task serves through a join resource: each shows as a token that can be taken off. |
|
|
240
240
|
| `Segmented`, `MenuSelect` | A switch between a few short options in a row (`size`, `iconOnly`), and the same options in a menu. |
|
|
@@ -99,7 +99,7 @@ In an app with [workflows](workflows.md), the endpoint also offers tools to find
|
|
|
99
99
|
| `get_workflow_run` | A run's status, input, output, error and cause, with its steps in the order they ran, each with its state, scope, try, status, input, output, assigned variables, logs, error and cause; `state` reads one state's steps, a page of 20 by default. |
|
|
100
100
|
| `create_workflow` | Creates a workflow from its `name`, `description` and `definition`, and returns its `id`, `version` and ETag. |
|
|
101
101
|
| `update_workflow` | Changes a workflow's definition by an `edit` or whole, as `definition`, and its `name` or `description`, and returns its `id`, new `version` and ETag. With the `etag` that `get_workflow` gave, it fails when someone changed the workflow since; an edit without one is saved against the version it was applied to. |
|
|
102
|
-
| `run_workflow` | Starts a run of a workflow's current version with an `input`, and returns its `run_id` and `status`. |
|
|
102
|
+
| `run_workflow` | Starts a run of a workflow's current version with an `input`, and returns its `run_id` and `status`. With `rerun_of`, a failed run's id, it [re-runs](workflows.md#re-running-a-failed-run) that run from its failed step instead, on its input. |
|
|
103
103
|
|
|
104
104
|
An `edit` is a [JSON Merge Patch](https://www.rfc-editor.org/rfc/rfc7396) of the saved definition: only what changes, at its place in the document. An object merges into the object there, `null` removes a member, and any other value, a list included, replaces it. Changing one argument of one state:
|
|
105
105
|
|
|
@@ -107,7 +107,7 @@ An `edit` is a [JSON Merge Patch](https://www.rfc-editor.org/rfc/rfc7396) of the
|
|
|
107
107
|
{ "id": "...", "edit": { "stateMachine": { "States": { "Notify": { "Arguments": { "url": "https://hooks.example.com/v2" } } } } } }
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
A definition the save check refuses comes back as `{ "error": "invalid-workflow", "message": ..., "problems": [...] }`, each problem `{ state, field, message }` naming the state, the field and what was expected, as `validate_workflow` gives them; nothing is saved. A run goes on in the background on the app's jobs, so `run_workflow` returns while it is queued: the client reads it with `get_workflow_run` until it has `succeeded` or `failed`, fixes the step that failed and runs
|
|
110
|
+
A definition the save check refuses comes back as `{ "error": "invalid-workflow", "message": ..., "problems": [...] }`, each problem `{ state, field, message }` naming the state, the field and what was expected, as `validate_workflow` gives them; nothing is saved. A run goes on in the background on the app's jobs, so `run_workflow` returns while it is queued: the client reads it with `get_workflow_run` until it has `succeeded` or `failed`, fixes the step that failed and re-runs the run from that step.
|
|
111
111
|
|
|
112
112
|
`create_workflow`, `update_workflow` and `run_workflow` come with [writes](#writes), and the person approves each first, as any write, unless they hold one of the [roles without approval](#writes-without-approval); with writes limited to some resources, they save on `workflows` and run on `workflow_runs` only when the list names them. A run does what its steps do, such as requests, mail and record writes, as the workflow's last editor.
|
|
113
113
|
|
|
@@ -149,6 +149,10 @@ Each part gets:
|
|
|
149
149
|
|
|
150
150
|
[Adding a field type](../guides/field-type.md) builds one step by step.
|
|
151
151
|
|
|
152
|
+
### JSON
|
|
153
|
+
|
|
154
|
+
`json` is a field type Protobase brings for any JSON field a form should edit, such as a workflow run's `input`: `r.input.widget('json')`. The form edits the value as JSON text, the record page shows it written out on several lines, and a list's table on one. Text that does not parse yet says so under the box and leaves the value as it last parsed; an emptied box is null. It takes the form's whole width. A plugin that registers a field type named `json` replaces it.
|
|
155
|
+
|
|
152
156
|
### Members
|
|
153
157
|
|
|
154
158
|
`members` is a field type Protobase brings, for a JSON field that holds an array of user ids, such as who may read a private channel. No plugin registers it; a view picks it as it picks any other:
|
|
@@ -163,6 +167,12 @@ view<typeof channels>('channels').fields((r) => ({ member_ids: r.member_ids.widg
|
|
|
163
167
|
|
|
164
168
|
The create form and the record's form pick the members of the organization the session works in by name: typing narrows them, each choice is a token whose button takes it off again, and choosing a chosen member takes them off too. It stores their user ids, the ids `.author()` stores and [`useMembers()`](custom-components.md#members-names) gives, never membership ids. The record page, cards and the list's table show each member by avatar and name; an id that is no longer a member shows as an unknown member. A JSON field is on a form only through a field type that edits it, so without the hint `member_ids` stays read-only. The field needs [organizations](auth.md#organizations); in an app without them it says so in place of the members. <gap>The members picker cannot search on the server because a server-side people resource is not implemented, so it loads the whole list in the browser, as `useMembers()` does.</gap> A plugin that registers a field type named `members` replaces it, and the server refuses to start when a view picks it for a field that is no JSON.
|
|
165
169
|
|
|
170
|
+
### Workflow
|
|
171
|
+
|
|
172
|
+
`workflow` is the field type the `workflows` resource draws its `definition` with: the [workflow editor](workflows.md#the-editor). Another JSON field holding a workflow's definition picks it as `r.definition.widget('workflow')`. It takes the form's whole width, and a list's table shows how many steps the workflow has. A plugin that registers a field type named `workflow` replaces it.
|
|
173
|
+
|
|
174
|
+
`workflow_run` is the field type the `workflow_runs` resource draws its own `id` with: the run's [trace](workflows.md#following-and-debugging-a-run) on its workflow's graph, with the chosen step's records under it. Another uuid field holding a run's id picks it as `r.run_id.widget('workflow_run')`. It takes the whole width, and a list's table shows the id. A plugin that registers a field type named `workflow_run` replaces it.
|
|
175
|
+
|
|
166
176
|
## Building
|
|
167
177
|
|
|
168
178
|
```sh
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Workflows written as state machines in the States Language with JSONata, checked on every save, run step by step on the app's jobs, with every step recorded.
|
|
6
6
|
|
|
7
7
|
:::caution[Beta]
|
|
8
|
-
Workflows run when they are started through the API, over MCP
|
|
8
|
+
Workflows run when they are started through the API, over MCP, from the app's code or by a test run in the admin app. Triggers and sign-in connections to other services are not built yet.
|
|
9
9
|
:::
|
|
10
10
|
|
|
11
11
|
A **workflow** is a graph of steps that the server runs one after another along its edges: fetch something, decide, run a step per item, merge the results, write records, send mail. It is written in the **States Language**, the JSON language of state machines, with **JSONata** as its expression language, in a profile of it that reads every value by name. A language model writes and changes it as easily as a person, and the same document is what the server checks and runs. Each run records every step it takes, with what the step read, gave and logged.
|
|
@@ -24,7 +24,7 @@ It adds four resources, over a `workflows` schema that [`protobase migration`](c
|
|
|
24
24
|
| --- | --- |
|
|
25
25
|
| `workflows` | A name, a description and the definition. Each save that changes the definition is checked, and kept as the workflow's next version, saved by the caller. |
|
|
26
26
|
| `workflow_versions` | Every version of every definition as it was saved, with who saved it and when. Read-only. |
|
|
27
|
-
| `workflow_runs` | A run, started by creating it with `workflow_id` and an `input
|
|
27
|
+
| `workflow_runs` | A run, started by creating it with `workflow_id` and an `input`, or with `rerun_of` to [re-run a failed run](#re-running-a-failed-run); it runs the version its workflow had then. Its status, output, error and timing are written by the runner. |
|
|
28
28
|
| `workflow_steps` | Every step of every run. Read-only. |
|
|
29
29
|
|
|
30
30
|
| `workflows` | |
|
|
@@ -79,7 +79,7 @@ A definition is one JSON document: the workflow's trigger, and the state machine
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
The document holds no layout:
|
|
82
|
+
The document holds no layout: the [editor](#the-editor) lays the graph out from its edges.
|
|
83
83
|
|
|
84
84
|
### Triggers
|
|
85
85
|
|
|
@@ -93,7 +93,7 @@ The document holds no layout: an editor lays the graph out from its edges.
|
|
|
93
93
|
| `"record": { "events": ["<resource>.<event>"] }` | on each of those record events, such as `deals.created` | `{ event, resource, key, after, before, changed }` |
|
|
94
94
|
| `"manual": {}` | when someone runs the workflow by hand | the input they give |
|
|
95
95
|
|
|
96
|
-
A save checks a trigger's shape; runs start through the API, over MCP
|
|
96
|
+
A save checks a trigger's shape; runs start through the API, over MCP, from the app's code or by a [test run](#test-runs). <gap>A webhook, schedule, mail or record trigger cannot start runs because the triggers are not implemented.</gap>
|
|
97
97
|
|
|
98
98
|
### Reading values by name
|
|
99
99
|
|
|
@@ -141,6 +141,19 @@ A state's output is its `Output`, else its result (a Task or Map) or the input i
|
|
|
141
141
|
|
|
142
142
|
A failing Task or Map fails with an error name: a step type's own, such as `Http.Status`, `States.Timeout` when it ran past its timeout, `States.TaskFailed` for any other error it threw, and `States.QueryEvaluationError` for an expression that did not evaluate or ran past its limits. The first retrier whose `ErrorEquals` names it tries the state again after `IntervalSeconds` (default 1), times `BackoffRate` (default 2) for each earlier retry, up to `MaxDelaySeconds`; `MaxAttempts` counts the retries after the first try (default 3, 0 for none). Without `Retry` a failure is not retried. Once no retry is left, the first catcher that names it goes on to its `Next`; without one the run fails. `States.ALL` names every error but `States.DataLimitExceeded` and `States.Runtime`, which end the run even where a retrier or catcher names them.
|
|
143
143
|
|
|
144
|
+
## The editor
|
|
145
|
+
|
|
146
|
+
A workflow's record page in the admin app, `/workflows/<id>`, and its create form draw the definition as a graph to edit, and save the same document the API saves:
|
|
147
|
+
|
|
148
|
+
- **The graph lays itself out.** The trigger is on top, each state is a box, a Map is a box around the states it runs for each item, and each edge is drawn from the field that names it: a state's `Next`, a Choice's rules (labelled with their condition) and its `Default` ("Otherwise"), and a Task's or Map's catchers ("On error", dashed). The graph is laid out by ELK's layered algorithm, from top to bottom, each time a state or edge is added, removed or renamed; the document keeps no positions, so the graph a person sees is the one a model or the API wrote, and the one they edit is what a model reads back. The page scrolls under the wheel; the canvas zooms with its buttons and by pinching.
|
|
149
|
+
- **Adding a step.** *Add a step* adds a Task of a step type, or a Choice, Map, Wait, Pass, Succeed or Fail, named `Step 1`, `Step 2` and so on. It goes into the Map chosen, or the Map the chosen step is in, else into the workflow; it ends the run, or its item, until it is connected, and in a workflow or Map without steps it is the first.
|
|
150
|
+
- **Connecting steps.** Drag from a step's handle to another step, or tap one handle and then the other: the bottom handle sets `Next`, a Choice's *When* handle adds a rule and its *Otherwise* handle sets `Default`, a Task's or Map's *On error* handle adds a catcher of `States.ALL`, and the trigger's handle sets `StartAt`. A step leads only to steps of its own machine, so an edge into or out of a Map is refused. The chosen step's form sets the same edges with menus: *Goes on to*, each rule's and catcher's *Then goes to*, and *Otherwise goes to*.
|
|
151
|
+
- **Removing.** Delete or Backspace removes the chosen step or edge, as does the form's *Remove step*. Removing a step removes the edges to it: a `Next` to it becomes `End`, and a rule, `Default` or catcher naming it goes. Removing the first step of a workflow or Map makes the first step left in it the first.
|
|
152
|
+
- **A step's form.** Choosing a step opens its form under the graph: its name, which renames every edge to it, then what its type takes. A Task's form has a field for each argument of its step type, the ones it needs marked, as listed under [Step types](#step-types); a Choice's its rules and `Default`; a Map's `Items` and `MaxConcurrency`; a Wait's `Seconds`; a Pass's and a Succeed's `Output`; a Fail's `Error` and `Cause`. A field takes text, an expression (`{% ... %}`) or JSON; an emptied field is left out. *The step as JSON* edits every field of the step, such as `Retry`, `Assign` and `TimeoutSeconds`. The trigger's form picks the trigger and its schedule, mailbox or events.
|
|
153
|
+
- **Problems on their step.** The editor runs the same check as a save on every change, and shows each problem on the step it names, as a count on its box and a list and a marked field in its form; a problem with the trigger or the first step shows on the trigger, and one about the whole document above the graph. Saving writes the definition through the API, where the save hook checks it again: a definition with a problem is refused with the 400 above, whose `errors` hold each problem for the `definition` field, with its state and field in `params`.
|
|
154
|
+
|
|
155
|
+
A save also refuses a Task that leaves out an argument its step type needs, such as `app:http` without `url`, unless its `Arguments` is one expression, whose value is known only when it runs. A user who may read a workflow but not change it sees the same graph and forms, locked. <gap>The editor cannot undo a single change because an undo history is not implemented.</gap>
|
|
156
|
+
|
|
144
157
|
## Step types
|
|
145
158
|
|
|
146
159
|
### app:http
|
|
@@ -191,15 +204,43 @@ A file value is `{ uri, name, type, size }`, the handle to bytes in the app's st
|
|
|
191
204
|
|
|
192
205
|
A run starts when it is created, through the API or the admin app, in the same transaction: `POST /api/v1/workflow_runs` with `{ "workflow_id": "...", "input": { ... } }`. The input is at most 1 MiB as JSON and nests at most 64 levels; a larger one is refused with `input-too-large` or `input-too-deep` and no run. From the app's own code, such as a job or a write hook, `startWorkflow(db, { workflow, input, tenant, startedBy })` from `@protobase/server` starts one in `db`'s transaction, so it starts when the caller's writes commit, or not at all. With a `key`, such as `orders:42`, a workflow runs once per key: a start with a key it has a run for already, in this transaction or another, starts nothing and returns that run's id.
|
|
193
206
|
|
|
194
|
-
Each step is
|
|
207
|
+
Each step is recorded as a `workflow_steps` record: the state, its type, the scope it ran in (empty for the run itself, `Each.3[0]` for item 0 of the Map `Each` that was step 3), its number, its try, its status (`succeeded`, `retrying`, `caught` or `failed`), its input, output and assigned variables, its logs, when it started and ended and how long it took, its error and cause, and where it goes on to. A run's status is `queued`, `running`, `waiting` (with the time it goes on), `succeeded` with its output, or `failed` with its error and cause.
|
|
195
208
|
|
|
196
209
|
### Secrets
|
|
197
210
|
|
|
198
211
|
A workflow reads a secret only by name, `$states.context.Secrets.TICKETS_TOKEN`, and only one the config's `workflows.secrets` lists; the values are never part of the definition. Every value of the listed secrets is blanked out as `[redacted]` wherever it appears in what a run records: a step's input, output, variables, logs, error and cause, and the run's output, error and cause. In a Task's recorded input, every field whose name says it holds a secret, such as `Authorization`, `token` or `apiKey`, is blanked out too. A value an expression computes from a secret, however it changes it, such as `'Basic ' & $base64encode(...)`, is kept whole as `[redacted]`: in a Task's `Arguments` the step type gets the value and the record keeps `[redacted]`; in an `Output`, an `Assign`, a Map's `Items` or a Fail's `Error` and `Cause` the value itself is `[redacted]`, so later states read that. Read a secret in the `Arguments` of the Task that sends it. A secret that comes back changed in a step's result, such as one a server encodes in its answer, is not the value the runner looks for, and is kept as it came. A run's input is kept as its caller gave it, so a secret's value never belongs in it.
|
|
199
212
|
|
|
213
|
+
### Following and debugging a run
|
|
214
|
+
|
|
215
|
+
A workflow's page in the admin app lists its runs, newest first, with their status, who or what started them, when, how long they took, their version and error. A run's page, `/workflow_runs/<id>`, draws the workflow's graph as the run's version has it, the `workflow_run` [field type](plugins.md#workflow) on the run's `id`, with a mark on each state:
|
|
216
|
+
|
|
217
|
+
| Mark | |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| Succeeded, Caught, Failed | How the state's last step ended; for a state inside a Map, the worst of its items. A failed state's box is red. |
|
|
220
|
+
| Retrying | Its last try failed and a retrier tries it again. |
|
|
221
|
+
| Running | The run is on it, or goes on to it next. |
|
|
222
|
+
|
|
223
|
+
A state that took more than one step, by its tries, its visits or a Map's items, says how many. Choosing a state lists its steps under the graph in the order they ran, 20 at a time with *Show more*: each with its try, the Map item it ran in, its status, when it started and how long it took, its error and cause, input, output, the variables it assigned and its logs, and a link to the step's own page. Choosing the trigger shows the run's input. The page opens on the state that failed, the innermost one when a Map's item failed, so a failed run shows its error and logs at once.
|
|
224
|
+
|
|
225
|
+
While a run is queued, running or waiting, the page reads the run and its steps again every second, through the API as the person, so the marks, the step lists and the run's fields change as it goes, without a reload; it stops once the run has ended. The graph marks the states of a run's first 500 steps; the run's *Steps* list has them all. <gap>The graph cannot mark the steps after a run's first 500 because reading a run's steps in more than one page for the trace is not implemented.</gap> Everything the page shows is what the run recorded, so a [secret](#secrets) shows as `[redacted]` wherever it would have been.
|
|
226
|
+
|
|
227
|
+
### Re-running a failed run
|
|
228
|
+
|
|
229
|
+
A failed run's page offers *Re-run from the failed step*. It opens the form for a new run with `workflow_id` and `rerun_of` filled in; creating it, through the form or `POST /api/v1/workflow_runs` with `{ "workflow_id": "...", "rerun_of": "..." }`, starts a run that:
|
|
230
|
+
|
|
231
|
+
- runs on the workflow's version now, so fix the step first, and on the earlier run's input: a re-run given an `input` of its own is refused with `rerun-input`;
|
|
232
|
+
- keeps the earlier run's steps, copied as they were, but the try that failed and that state's retried tries before it, so the failed state runs again from its first try and none of the steps before it runs again. A Map that runs again keeps its finished items and runs again only those that had not finished, each from where it stood;
|
|
233
|
+
- is refused, and nothing starts, when the earlier run is not one of the workflow's runs the person may read (404), did not fail (`run-not-failed`), or would go on at a state the version no longer has (`rerun-refused`, naming the state).
|
|
234
|
+
|
|
235
|
+
A kept step's card on the run's page says it is from the earlier run, as it finished before the re-run started; the earlier run's page lists its re-runs.
|
|
236
|
+
|
|
237
|
+
### Test runs
|
|
238
|
+
|
|
239
|
+
A workflow's page offers *Test run*: the form for a new run of the workflow, whose `input` takes a sample input as JSON, which every state reads as `$states.context.Execution.Input`. Creating it opens the run's page, which follows it as it runs. A test run is a run like any other: its steps do what they do, as the workflow's last editor, with the version saved last.
|
|
240
|
+
|
|
200
241
|
## Over MCP
|
|
201
242
|
|
|
202
|
-
An AI client signed in to the app's [MCP endpoint](mcp.md#workflows) finds, makes, changes and test-runs workflows as the person: it lists the step types and triggers with their arguments, reads and validates definitions, saves one whole or by a small edit, starts a run with an input,
|
|
243
|
+
An AI client signed in to the app's [MCP endpoint](mcp.md#workflows) finds, makes, changes and test-runs workflows as the person: it lists the step types and triggers with their arguments, reads and validates definitions, saves one whole or by a small edit, starts a run with an input, reads each of the run's steps with its input, output, logs and error, and re-runs a failed run from its failed step once the workflow is fixed. The endpoint's instructions describe this language, and a refused definition comes back with its problems, each naming the state and field.
|
|
203
244
|
|
|
204
245
|
## Who may do what
|
|
205
246
|
|
|
@@ -225,8 +266,8 @@ All four are scoped to the tenant like any resource with `.tenant()`. A run take
|
|
|
225
266
|
The runner is `@protobase/workflows`, a package with no other Protobase dependency, over a narrow port: where runs and steps are kept (here the `workflows` schema), the queue that runs them (here the app's [jobs](jobs.md)), and the step types.
|
|
226
267
|
|
|
227
268
|
- A run is run in **segments**, each a `workflows.segment` job whose payload is the run's id alone: one runs the run from where it stands to its next wait, then queues the next. A Wait whose time is still ahead, or a retry's delay, saves its step with the time the run goes on, ends the segment and queues the next one for that time, so a waiting run holds no worker. A segment hands the rest of the run on after a minute of steps.
|
|
228
|
-
- Where a run stands is read from its saved steps.
|
|
229
|
-
- A worker that stops or dies mid-step leaves that step unsaved: its job is tried again and takes the run up from its last saved step, so a saved step never runs again, and the step that was in progress runs **at least once
|
|
269
|
+
- Where a run stands is read from its saved steps. A Task's step, and one with a time the run goes on at, is saved before the next state runs; the steps of other states are kept and saved with it, or as the segment ends. Each save is **one statement**, unique by each step's scope and number, so a second segment of the same run that finds one taken stops.
|
|
270
|
+
- A worker that stops or dies mid-step leaves that step unsaved: its job is tried again and takes the run up from its last saved step, so a saved step never runs again, and the step that was in progress runs **at least once**, as do the steps kept since the last save, which give the same records again. A step whose effect happened before its result was saved, such as a request that was sent, happens again. Every try of a step gets the same idempotency key, for a step type whose receiver dedupes by it, and `app:mail.send` makes it the mail's Message-ID. A job whose worker died is tried again once its timeout, 10 minutes, has passed.
|
|
230
271
|
- A Map's items run in the segment that reaches them, each item's steps saved under its own scope; an item that waits ends the segment once the others running settle, and the next segment takes each item up where it stands. A Map's `Items` is evaluated again by a segment that takes up an unfinished Map.
|
|
231
272
|
- JSONata runs in the server's process under a time and a depth limit per expression; it reaches no file, network or code of the app.
|
|
232
273
|
|