The screen a run is driven from: rendering the current step, collecting input, and completing it.
Route: /process/[processId]
Renders the current step of a running process, collects input, and completes the step.
For input steps that means a form; for every other type it shows progress while the
worker does the work.
It also hosts everything wrapped around a run: the step rail, past-step browsing, sharing, export, audit, and the completion view.
| Component | Role |
|---|---|
page.tsx | Orchestration, polling, form state, completion. |
ProcessSharingPanel.tsx | Link visibility and per-user assignments. |
StepInputControl.tsx | Renders one field — dispatches on the 11 field types. |
ItemListEditor.tsx | Repeating rows for item_list, including nesting. |
ProcessFileField.tsx | Upload and attachment display. |
StepInputsForm.tsx | Assembles fields into the step form. |
PastStepReadonlyView.tsx | Read-only view of a completed step. |
ProcessStepRail.tsx | Step list, grouped by phase. |
ProcessCompletedView.tsx | Terminal screen, renders resultViewControls. |
StepRequiredPermissions.tsx | Shows which permissions the current step needs. |
ProcessAuditStateButton.tsx, ProcessExportButton.tsx | Entry points to audit and export. |
Siblings: /processes and the dashboard, and
/start/[templateKey] (collects runInputs, evaluates startExpression).
viewedStepIndex lets you open any step while the run sits somewhere else. What you
get depends on whether that step ran:
| Step | View |
|---|---|
| Completed | PastStepReadonlyView — the values it captured, read-only |
Skipped by enabledExpression | UpcomingStepPreview — what it would have asked |
| Still ahead | UpcomingStepPreview — what it will ask, no values |
Selecting the live step clears the selection and returns to it, and the selection resets once the run reaches the step being viewed.
The live step is always steps[steps.length - 1]. Everything before it is history.
Section headers live in the same inputs[] array as fields, so a long step reads as one
flat list. groupFieldsBySection (platform/forms/src/step-field-groups.ts) turns that array into
the structure the headers imply — a section header opens a card, a subsection opens a
block inside it, and fields before any header render bare, so a step without headers looks
exactly as it always did.
A section header carrying no fields of its own is read as a title for the sections
below it rather than an empty card. ib-payouts is authored this way: "Ethereum Mainnet"
over "Past week", "Transfers" over the import sections. sectionNav turns the same
structure into the rail's table of contents, so the nav nests instead of listing peers.
The runner form, the editor preview and the editor canvas all consume these helpers, so the three cannot drift.
An automatic step advances in about a second, so its live view is rarely seen — the place
people meet it is the past-step view. Both render AutomaticStepDetails
(ui/generic/src/components/AutomaticStepDetails.tsx), which shows what the step is configured to do
from its own settings: the Slack channel and message, a Decision's expression and
branches, a Wait's schedule. The past view adds the outcome read from { ok, at, error },
with the raw context behind a Raw step output toggle.
The editor preview renders the same component, so the preview stays a mirror. The one difference it cannot close: the runner resolves expressions against real context, the editor cannot, so the editor labels them as written.
StepInputControl is the single switch over all 11 field types. A new field type is
added here and in the editor's config panel — nowhere else in the runner.
A field with readOnly: true renders resolved defaultValue text rather than an input.
This is how templates display computed values, generated payloads, and instructions
inline — there is no separate "display" field type. downloadFilename turns one into a
download button.
The runner drives two stages directly:
PUT /api/process/{id}/steps/{stepId} → updateStep
POST /api/process/{id}/steps/{stepId}/complete → completeStep
Writes address the step instance id, not the step key — a looping run visits the
same stepKey repeatedly and each visit is a separate instance.
Everything else the runner shows — a slack_notify step in flight, a wait step
parked — is the worker's executeStep happening elsewhere. The runner polls and
re-renders.
step.permissions[] decides who may act. StepRequiredPermissions surfaces this so
a blocked user sees why rather than a dead form.
completeExpression decides whether Continue/Finish appears at all. This is
independent of permission — a user may be allowed to edit a step and still be unable to
finish it because the data is incomplete.
Both are enforced server-side. The API returns a single 403 for either case with a
deliberately merged message, so the UI cannot distinguish "not allowed" from "not
complete."
Per-field permissions mean the form can be partly editable: fields the user lacks permission for render read-only, and writes are filtered to permitted keys before the merge — a rejected field write is dropped silently rather than erroring.
ProcessSharingPanel introduces vocabulary used nowhere else. See
sharing.
failProcess() sets status = "completed" and populates error. The runner must read
error separately to know a run died. Anything filtering on status alone shows failures
as successes. See
known issues.
(Notification delivery is no longer one of these. A notify step that failed to send is marked in the rail, shows Not sent with the reason when opened, and is listed on the completion screen. The run still advances, which is the soft-failure contract working as intended — see step types.)