@protobase/cli 0.67.9 → 0.67.11
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/workflows.md +3 -1
- package/skills/protobase/references/reference/custom-components.md +2 -2
- package/skills/protobase/references/reference/mcp.md +27 -1
- package/skills/protobase/references/reference/plugins.md +4 -0
- package/skills/protobase/references/reference/workflows.md +25 -6
|
@@ -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.11",
|
|
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.11",
|
|
47
|
+
"@protobase/layout": "0.67.11",
|
|
48
|
+
"@protobase/query": "0.67.11",
|
|
49
|
+
"@protobase/schema": "0.67.11",
|
|
50
|
+
"@protobase/server": "0.67.11",
|
|
51
|
+
"@protobase/ui": "0.67.11",
|
|
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",
|
|
@@ -6,7 +6,8 @@ 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.
|
|
@@ -14,3 +15,4 @@ Graphs of steps the server runs along their edges, written in a language a model
|
|
|
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.
|
|
15
16
|
- [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
17
|
- [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.
|
|
18
|
+
- [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.
|
|
@@ -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. |
|
|
@@ -86,6 +86,31 @@ When a resource takes [comments](comments.md), the endpoint also offers `list_co
|
|
|
86
86
|
|
|
87
87
|
`add_comment` is a write: the person approves each comment first, seeing the record's name and the whole comment, unless they hold one of the [roles without approval](#writes-without-approval). With writes limited to some resources, it comments only on those. A comment it adds mentions nobody, so it sends no emails. An empty comment, one over 10,000 characters, a resource without comments, and a record the person may not read, or one that is soft-deleted, are refused before asking.
|
|
88
88
|
|
|
89
|
+
### Workflows
|
|
90
|
+
|
|
91
|
+
In an app with [workflows](workflows.md), the endpoint also offers tools to find, make, change and test-run them, and its instructions describe the workflow language: the document, the states, reading values by name, errors and retries, with an example. A client can write a working workflow from them and the tools alone, without the docs. Each tool goes through the workflow resources as the person, so their [grants](workflows.md#who-may-do-what) and tenant apply: someone without them is answered as for a resource that is not there, and nothing changes.
|
|
92
|
+
|
|
93
|
+
| Tool | What it does |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `list_workflow_types` | The step types a Task names as its `Resource`, each with its arguments (required or not, and what each takes), its result and the errors it fails with; and the triggers, how the definition holds each and the input a run gets from it. |
|
|
96
|
+
| `list_workflows` | A page of the workflows the person may read, newest change first, without their definitions: `filter` on `name`, `page_size` (20 by default, at most 100) and `page_token`. |
|
|
97
|
+
| `get_workflow` | One workflow with its whole definition and its ETag. |
|
|
98
|
+
| `validate_workflow` | Checks a `definition`, or a workflow's `id` with an `edit` of its saved definition, as a save checks it, and saves nothing: `{ valid: true }`, or `{ valid: false, problems }`. |
|
|
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
|
+
| `create_workflow` | Creates a workflow from its `name`, `description` and `definition`, and returns its `id`, `version` and ETag. |
|
|
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`. |
|
|
103
|
+
|
|
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
|
+
|
|
106
|
+
```json
|
|
107
|
+
{ "id": "...", "edit": { "stateMachine": { "States": { "Notify": { "Arguments": { "url": "https://hooks.example.com/v2" } } } } } }
|
|
108
|
+
```
|
|
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 it again.
|
|
111
|
+
|
|
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
|
+
|
|
89
114
|
### SQL for admins
|
|
90
115
|
|
|
91
116
|
`mcpEndpoint.sql` also offers `run_read_only_query`, as in the [assistant](assistant.md#the-built-in-backend), to people with the `admin` or `ai` role:
|
|
@@ -255,7 +280,8 @@ The endpoint serves each request on its own, without an MCP session: a `GET` or
|
|
|
255
280
|
|
|
256
281
|
- `mcpHandler({ authenticate, resource, scope, server })`: the endpoint as a fetch handler. It checks the host, admits a valid token (`mcpGate`) and serves the request with the server that `server(session, request)` builds for its caller; `request` is the HTTP request, such as for the language the person reads.
|
|
257
282
|
- `mcpServer({ info, instructions, tools, context, report, rolesWithoutApproval, approvals })`: an MCP server named by `info`, its `name`, `version` and `icons`, such as `mcpIcons(origin, icons)`, the app's icons on `origin`, and offering [assistant tools](assistant.md#the-apps-own-tools), each with its JSON Schema, its `annotations`, MCP's tool hints, and its `title` and `_meta`, and each refusing as [the app's own tools](#the-apps-own-tools) do. `context` is the tools' context without `approve`, with the `locale` its approvals are asked in (English when absent): each call gets its own, which asks the client's user, or approves at once when the session's user holds one of `rolesWithoutApproval`. It is a `ToolContext`, with the app's `records`, `resources()` and `show`, unless every tool is declared with a smaller one, such as `defineTool<BaseToolContext>(...)`, whose context is only `session`, `locale` and `approve`: a host without app records then passes `{ session }`. `report` gets what a tool throws other than a refusal. `approvals` is `mcpApprovals()`, built once and passed to every server the endpoint builds: it keeps the approvals asked for until they are answered or expire. `instructions` is what it tells clients when they connect, by default `mcpInstructions`, the advice on reading records above.
|
|
258
|
-
- `mcpTools({ db, sql, writes, commented, own })`: the endpoint's tools for a session; `writes` is `true`, `false` or `{ resources, rolesWithoutApproval }`, `commented` names the resources with comments, and `own` are [the app's own tools](#the-apps-own-tools), offered after the rest.
|
|
283
|
+
- `mcpTools({ db, sql, writes, commented, workflows, own })`: the endpoint's tools for a session; `writes` is `true`, `false` or `{ resources, rolesWithoutApproval }`, `commented` names the resources with comments, `workflows: true` offers the [workflow tools](#workflows), and `own` are [the app's own tools](#the-apps-own-tools), offered after the rest.
|
|
284
|
+
- `workflowReadTools()` and `workflowWriteTools({ resources })`: the workflow tools alone, and `workflowInstructions`, what the endpoint tells clients about the language, to append to `mcpInstructions`.
|
|
259
285
|
- `readRecordTools()`, or `listResourcesTool()`, `describeResourceTool()`, `listRecordsTool()` and `getRecordTool()`: the read tools alone; `readOnlyQueryTool(db)` is `run_read_only_query`, and `readWriteQueryTool(db)` proposes one write statement that the user approves before it runs.
|
|
260
286
|
- `commentTools({ commented }, writes)`, or `listCommentsTool({ commented })` and `addCommentTool({ commented, resources })`: the comment tools on the resources named in `commented`, which call the context's `comments`, and `add_comment` only when `writes` is not `false`.
|
|
261
287
|
- `writeRecordTools({ resources })`, or `createRecordTool(...)`, `updateRecordTool(...)` and `deleteRecordTool(...)`: the single write tools alone, on the listed resources or, without `resources`, on any; `batchWriteTool({ resources })` is `batch_write` alone.
|
|
@@ -163,6 +163,10 @@ view<typeof channels>('channels').fields((r) => ({ member_ids: r.member_ids.widg
|
|
|
163
163
|
|
|
164
164
|
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
165
|
|
|
166
|
+
### Workflow
|
|
167
|
+
|
|
168
|
+
`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.
|
|
169
|
+
|
|
166
170
|
## Building
|
|
167
171
|
|
|
168
172
|
```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 or from the app's code. Triggers
|
|
8
|
+
Workflows run when they are started through the API, over MCP or from the app's code. 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.
|
|
@@ -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 or the app's code. <gap>A webhook, schedule, mail or record trigger cannot start runs because the triggers are not implemented.</gap>
|
|
96
|
+
A save checks a trigger's shape; runs start through the API, over MCP or from the app's code. <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
|
|
|
@@ -121,6 +121,8 @@ The save is refused, naming each state and field, when the document does not fit
|
|
|
121
121
|
|
|
122
122
|
The problem's `problems` lists them as `{ state, field, message }`, where a state inside a Map is named by its path, such as `Each/Total`. The document's JSON Schema is `definitionSchema` of `@protobase/workflows`, and `checkDefinition` runs the whole check.
|
|
123
123
|
|
|
124
|
+
<gap>A save cannot refuse a Task's missing or unknown argument because checking `Arguments` per step type is not implemented.</gap> The step fails its run instead, with an error such as `Http.InvalidArguments` whose cause names the argument.
|
|
125
|
+
|
|
124
126
|
### States
|
|
125
127
|
|
|
126
128
|
| Type | |
|
|
@@ -139,6 +141,19 @@ A state's output is its `Output`, else its result (a Task or Map) or the input i
|
|
|
139
141
|
|
|
140
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.
|
|
141
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
|
+
|
|
142
157
|
## Step types
|
|
143
158
|
|
|
144
159
|
### app:http
|
|
@@ -189,12 +204,16 @@ A file value is `{ uri, name, type, size }`, the handle to bytes in the app's st
|
|
|
189
204
|
|
|
190
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.
|
|
191
206
|
|
|
192
|
-
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.
|
|
193
208
|
|
|
194
209
|
### Secrets
|
|
195
210
|
|
|
196
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.
|
|
197
212
|
|
|
213
|
+
## Over MCP
|
|
214
|
+
|
|
215
|
+
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, and reads each of the run's steps with its input, output, logs and error. The endpoint's instructions describe this language, and a refused definition comes back with its problems, each naming the state and field.
|
|
216
|
+
|
|
198
217
|
## Who may do what
|
|
199
218
|
|
|
200
219
|
The workflow resources take only [named grants](access.md#resources-that-need-named-grants): a role's wildcard such as `*.read` reaches none of them, since a run reads and sends data as its definition says.
|
|
@@ -219,8 +238,8 @@ All four are scoped to the tenant like any resource with `.tenant()`. A run take
|
|
|
219
238
|
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.
|
|
220
239
|
|
|
221
240
|
- 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.
|
|
222
|
-
- Where a run stands is read from its saved steps.
|
|
223
|
-
- 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
|
|
241
|
+
- 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.
|
|
242
|
+
- 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.
|
|
224
243
|
- 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.
|
|
225
244
|
- 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.
|
|
226
245
|
|