@protobase/cli 0.72.0 → 0.72.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.
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.72.0";
2
+ var version = "0.72.1";
3
3
  //#endregion
4
4
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/cli",
3
- "version": "0.72.0",
3
+ "version": "0.72.1",
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,13 +43,13 @@
43
43
  "dependencies": {
44
44
  "@better-auth/core": "^1.7.12",
45
45
  "@hono/node-server": "^2.1.3",
46
- "@protobase/config": "0.72.0",
47
- "@protobase/layout": "0.72.0",
48
- "@protobase/query": "0.72.0",
49
- "@protobase/schema": "0.72.0",
50
- "@protobase/server": "0.72.0",
51
- "@protobase/ui": "0.72.0",
52
- "@protobase/workflows": "0.72.0",
46
+ "@protobase/config": "0.72.1",
47
+ "@protobase/layout": "0.72.1",
48
+ "@protobase/query": "0.72.1",
49
+ "@protobase/schema": "0.72.1",
50
+ "@protobase/server": "0.72.1",
51
+ "@protobase/ui": "0.72.1",
52
+ "@protobase/workflows": "0.72.1",
53
53
  "@react-email/render": "^2.1.0",
54
54
  "@scalar/hono-api-reference": "^0.12.10",
55
55
  "@sentry/vite-plugin": "^11.10.0",
@@ -5,7 +5,9 @@
5
5
  The page of one record, with its sections, related records, galleries, charts and editing.
6
6
 
7
7
  - [Record page layout](../reference/ui-config.md#the-record-page): Sections, a summary sidebar, related tables, maps, charts, galleries, history, comments and your own components.
8
- - [Full-width record pages](../reference/ui-config.md#page-width): A view's record pages fill the whole area beside the sidebar instead of a centred column, with the sidebar cards still beside the sections.
8
+ - [Full-width record pages](../reference/ui-config.md#page-width): A view's record pages and create form fill the whole area beside the sidebar instead of a centred column, with the sidebar cards still beside the sections.
9
+ - [Page-wide sections](../reference/ui-config.md#page-wide-sections): A section spans the page above the main column and the sidebar cards, such as a workflow's graph, with Save in a bar at the foot of the window.
10
+ - [Record pages that open another](../reference/ui-config.md#a-record-page-that-opens-another): A record's page opens another address built from its fields in its place, such as a workflow run opening its workflow's page with the run shown.
9
11
  - [Full-area record pages](../reference/ui-config.md#full-area-record-pages) (beta): A view draws its record pages with a component of its own that fills the area beside the sidebar and under the top bar, such as a chat channel, and links to the standard page.
10
12
  - [Read view and edit](../reference/ui-config.md#read-view-and-edit): A record opens as a read-only fact sheet or a form, and Edit opens the form at its own address.
11
13
  - [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.
@@ -7,13 +7,14 @@ Graphs of steps the server runs along their edges, written in a language a model
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
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 of every trigger and step type of the app, its plugins' included, each with a form for its input, with every problem the save would refuse shown on its step as it is edited.
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 of every trigger and step type of the app, its plugins' included, each with a form for its input beside the graph, with every problem the save would refuse shown on its step as it is edited.
11
+ - [One page per workflow](../reference/workflows.md#the-workflows-page) (beta): A workflow's full-width page holds its graph, its saved versions to look at read-only, its runs shown on the graph and its trigger, so you build and debug without leaving it; the sidebar has only Workflows.
11
12
  - [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.
12
13
  - [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.
13
14
  - [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
15
  - [Mail step](../reference/workflows.md#appmailsend) (beta): Sends mail, with files the run downloaded attached, from the app's own sender.
15
16
  - [Runs and step records](../reference/workflows.md#runs-and-their-steps) (beta): Each run started by hand through the API, from the app's code or by a trigger, 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
+ - [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 shown on its workflow's page marks every step's status on the graph of the version it ran on, live while it runs, with each step's input, output, logs, timing, tries and error, secrets blanked out.
17
18
  - [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.
18
19
  - [Replay a run](../reference/workflows.md#replaying-a-run) (beta): A run that ended runs again from the start on its version and input without sending its outside writes, such as mail, record writes and requests other than GET, which give what they gave then, while its reads come from the step cache or run again, so you can work on what formed a message without sending it twice.
19
20
  - [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.
@@ -115,7 +115,7 @@ Every place in the app has an address, and the app writes and reads them in one
115
115
  | Address | Is |
116
116
  | --- | --- |
117
117
  | `{ kind: 'list', resource, filter?, orderBy?, display? }` | A resource's list, with the filter, order and [display](ui-config.md#list-displays) the list page would show. |
118
- | `{ kind: 'record', resource, key, edit?, open?, tab?, standard? }` | One record; `key` is its value, or its parts for a composite key. `edit: true` is its form (`?edit=true`), on a view whose record page opens as a [read view or locked](ui-config.md#read-view-and-edit). `open` lists the titles of the record page's collapsed [groups](ui-config.md#groups) that are open (`?open=<title>`, once per group), and `tab` the title of the [tab](ui-config.md#tabs) it shows (`?tab=<title>`). `standard: true` is its standard page (`?standard=true`), on a view whose record page is a [component of the app's own](ui-config.md#full-area-record-pages). |
118
+ | `{ kind: 'record', resource, key, edit?, open?, tab?, standard?, version?, run? }` | One record; `key` is its value, or its parts for a composite key. `edit: true` is its form (`?edit=true`), on a view whose record page opens as a [read view or locked](ui-config.md#read-view-and-edit). `open` lists the titles of the record page's collapsed [groups](ui-config.md#groups) that are open (`?open=<title>`, once per group), and `tab` the title of the [tab](ui-config.md#tabs) it shows (`?tab=<title>`). `standard: true` is its standard page (`?standard=true`), on a view whose record page is a [component of the app's own](ui-config.md#full-area-record-pages) or [opens another](ui-config.md#a-record-page-that-opens-another). On a workflow, `version` is the saved version its graph shows, read-only (`?version=<number>`), and `run` the run shown on it (`?run=<id>`); see [The workflow's page](workflows.md#the-workflows-page). |
119
119
  | `{ kind: 'create', resource, values? }` | The form for a new record, with `values` filled in by field name, as text: `{ kind: 'create', resource: 'notes', values: { listing_id: '{id}' } }` on a listing's page starts a note about that listing. A relation shows the name of the record it points at. The values are a start the person can change, not a rule: what they may write is still up to the access rules. A view with a [create page](ui-config.md#create-pages) shows that page here instead, with the values in the address. |
120
120
  | `{ kind: 'page', page, tab? }` | A composed page; `tab` the name of one of its [tabs](blocks.md#tabs-and-tab) (`/<page>/<tab>`), the first when omitted. |
121
121
 
@@ -674,12 +674,32 @@ A record opened from a list, by its row or its card, shows the fields the list l
674
674
 
675
675
  ### Page width
676
676
 
677
- The record page sits in a centred column about 1100px wide. `.recordWidth('full')` makes it fill the content pane beside the sidebar instead, with the sidebar cards still on the right, for a record with wide sections or related tables; `'column'` is the default. On a phone both look the same.
677
+ The record page sits in a centred column about 1100px wide. `.recordWidth('full')` makes it fill the content pane beside the sidebar instead, with the sidebar cards still on the right, for a record with wide sections or related tables, and makes its create form as wide; `'column'` is the default. On a phone both look the same.
678
678
 
679
679
  ```ts
680
680
  view<typeof objectives>('objectives').recordWidth('full')
681
681
  ```
682
682
 
683
+ ### Page-wide sections
684
+
685
+ `l.section(title, fields, { width: 'page' })` spans the page above the main column and the sidebar cards, for a field that needs the room, such as a workflow's graph; Save then stays in a bar at the foot of the window, on a wide screen as on a phone. It applies to a section outside tabs and groups. On the create form it is a section like the others.
686
+
687
+ ```ts
688
+ view<typeof workflows>('workflows')
689
+ .recordWidth('full')
690
+ .layout((r) => [l.section('Workflow', [r.name, r.description]), l.section('Graph', [r.definition], { width: 'page' })])
691
+ ```
692
+
693
+ ### A record page that opens another
694
+
695
+ `.recordOpens(to)` sends a record's page to another address in its place, its `{field}`s, in the resource, the key and `run`, filled from the record, replacing the record's address in the history so Back skips it: a workflow run's page opens its workflow's page with the run on the graph. A caller who cannot follow it, such as one who may not read the resource it names, gets the record's own page. The record's own page stays at its address with `?standard=true`.
696
+
697
+ ```ts
698
+ view<typeof workflowRuns>('workflow_runs').recordOpens({ kind: 'record', resource: 'workflows', key: '{workflow_id}', run: '{id}' })
699
+ ```
700
+
701
+ A record address can also carry `version`, an earlier saved version of the record shown by a field type that keeps versions, such as the `workflow` field type: the record page then shows its read view, without Save or Edit.
702
+
683
703
  ### Full-area record pages
684
704
 
685
705
  `.record({ component })` makes the record page at `/<resource>/<key>` a [custom component](custom-components.md#full-area-record-pages) of the app's own, for a record that is used rather than filled in, such as a chat channel. The component fills the content area, at full width beside the sidebar and down to the bottom bar on a phone, and the page does not scroll, so the component scrolls its own parts, such as messages above a box that stays at the bottom. The sidebar, the top bar with its breadcrumb naming the record, the bottom bar and a right panel stay; the heading, Save, the action rail, the sections, the related records and the Details card are not drawn. A record that does not exist, or that the user may not read, shows the record page's usual notice, and the component is never drawn.
@@ -206,13 +206,23 @@ A failing Task or Map fails with an error name: a step type's own, such as `Http
206
206
 
207
207
  ## The editor
208
208
 
209
- 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:
209
+ 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. In the admin app's sidebar, Workflows is the only entry: a workflow's versions, runs, steps and trigger are on its page, and their resources and API stay as they are.
210
+
211
+ ### The workflow's page
212
+
213
+ A workflow's page is full width, and so is its create form. The graph spans the page, with a side panel beside it on a wide screen and under it on a narrow one; the workflow's name, description and enabled version, its trigger and its runs follow below, and Save stays at the foot of the window. Choosing a step shows its form in the side panel, in view beside the graph.
214
+
215
+ - **Versions.** *Version*, above the graph, chooses which saved version the graph shows, each named with when it was saved, and the latest and the enabled one marked *(latest)* and *(enabled)*; it lists the latest 100. <gap>The version selector cannot offer a version older than the latest 100 because loading more of them into it is not implemented.</gap> Such a version still opens by its address. The latest version is the one you edit. An older one is read-only: no fields to fill in, no *Add a step*, no Save, and no editing on the graph. A bar above the graph, repeated at the top of the side panel, says in words which version is shown and which is the latest, with *Back to the latest*. The version is in the address, `/workflows/<id>?version=<number>`, so a link opens it.
216
+ - **Runs.** *Run* chooses one of the workflow's latest 20 runs, each named with when it started, its status and its version, and shows it on the graph, as [Following and debugging a run](#following-and-debugging-a-run) says. A run shows only on the version it ran on: choosing it sets *Version* to that version, choosing another version closes it, and the bar says which version it ran on, with *Close the run*. A run is only read, on the latest version too. The run is in the address, `?version=<number>&run=<id>`, and a run's own address, `/workflow_runs/<id>`, opens the workflow's page with the run on the graph.
217
+ - **Test run** starts a run, as [Test runs](#test-runs) says.
218
+
219
+ A user who may read a workflow but not change it gets the same page, its fields and graph locked.
210
220
 
211
221
  - **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.
212
222
  - **Adding a step.** *Add a step* adds a Task of any step type the app can run, its plugins' included, 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.
213
223
  - **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*.
214
224
  - **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.
215
- - **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's input schema, the ones it needs marked, as listed under [Step types](#step-types) or by its plugin: one line of text for a string, one line for a number or `true` and `false`, a menu for an `enum`, names separated by commas for a list of strings, and JSON for anything else; 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 among the app's triggers, its plugins' included, and draws a field for each input of its schema the same way, such as a record trigger's resource, `when` and `key`.
225
+ - **A step's form.** Choosing a step opens its form in the side panel beside 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's input schema, the ones it needs marked, as listed under [Step types](#step-types) or by its plugin: one line of text for a string, one line for a number or `true` and `false`, a menu for an `enum`, names separated by commas for a list of strings, and JSON for anything else; 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 among the app's triggers, its plugins' included, and draws a field for each input of its schema the same way, such as a record trigger's resource, `when` and `key`.
216
226
  - **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`.
217
227
 
218
228
  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> The editor reads the app's types from `GET /api/workflows/types`, `client.workflowTypes()` of `@protobase/client`: the same list `list_workflow_types` gives over MCP, `{ step_types, triggers }`, which only a user who may read workflows gets. A type the app cannot run, such as one that signs in through a connection whose OAuth client the app does not set, is not offered, and the check refuses it with the reason; a stored trigger of such a type, or of a type the app does not have, shows its type and input, read-only. The boxes, their accessible names and a run's step headings name each type by the label the list gives it, such as *Shout* for `acme:shout`.
@@ -301,7 +311,7 @@ A workflow reads a secret only by name, `$states.context.Secrets.TICKETS_TOKEN`,
301
311
 
302
312
  ### Following and debugging a run
303
313
 
304
- 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:
314
+ 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. Choosing one, in that list or in *Run* above the graph, shows it on the workflow's page: the graph as the run's version has it, with a mark on each state:
305
315
 
306
316
  | Mark | |
307
317
  | --- | --- |
@@ -309,23 +319,23 @@ A workflow's page in the admin app lists its runs, newest first, with their stat
309
319
  | Retrying | Its last try failed and a retrier tries it again. |
310
320
  | Running | The run is on it, or goes on to it next. |
311
321
 
312
- 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.
322
+ 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 in the side panel 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, and with nothing chosen the side panel shows the run itself: its status, when it started and ended, who started it, its error, input and output. The run's actions, such as *Re-run from the failed step* and *Replay*, stay at the top of the side panel, and *Open the run's own page* leads to the run's record page, `/workflow_runs/<id>?standard=true`, with its steps and re-runs listed, drawn with the `workflow_run` [field type](plugins.md#workflow) on the run's `id`. 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.
313
323
 
314
- 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.
324
+ 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 own page lists 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.
315
325
 
316
326
  ### Re-running a failed run
317
327
 
318
- 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:
328
+ A failed run, shown on its workflow'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:
319
329
 
320
330
  - 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`;
321
331
  - 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;
322
332
  - 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).
323
333
 
324
- 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.
334
+ A kept step's card says it is from the earlier run, as it finished before the re-run started; the earlier run's own page lists its re-runs.
325
335
 
326
336
  ### Replaying a run
327
337
 
328
- A run that ended, succeeded or failed, offers *Replay* on its page. It opens the form for a new run with `workflow_id`, `rerun_of` and `replay` filled in; creating it, through the form or `POST /api/v1/workflow_runs` with `{ "workflow_id": "...", "rerun_of": "...", "replay": true }`, starts a run that runs from the start on the replayed run's version and input, so you can look into what formed a message, such as a mail sent with the wrong contents, without sending it again:
338
+ A run that ended, succeeded or failed, offers *Replay* where it is shown. It opens the form for a new run with `workflow_id`, `rerun_of` and `replay` filled in; creating it, through the form or `POST /api/v1/workflow_runs` with `{ "workflow_id": "...", "rerun_of": "...", "replay": true }`, starts a run that runs from the start on the replayed run's version and input, so you can look into what formed a message, such as a mail sent with the wrong contents, without sending it again:
329
339
 
330
340
  - a step whose type writes, such as `app:mail.send`, `app:records.create`, `app:records.update`, an `app:http` request with any method but `GET`, and a plugin's step that does not say it only reads, is not called: it gives what the replayed run's step of the same place gave, its output and variables, or a `null` output when that run has none there, and its logs say *Not sent: replay*;
331
341
  - a step that only reads, such as `app:records.get`, `app:records.list`, an `app:http` `GET`, or a plugin's step that says so, is served from the [step cache](workflow-plugins.md#caching-results) when its type caches and an earlier result matches, saying so in its logs, with a line for each secret that changed since that result was kept; otherwise it runs again, as the same people a run of that version acts as;
@@ -335,7 +345,7 @@ A replay given an `input` of its own is refused with `rerun-input`, and one with
335
345
 
336
346
  ### Test runs
337
347
 
338
- 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.
348
+ 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 workflow's page with the run on the graph, followed 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.
339
349
 
340
350
  ## Over MCP
341
351