Flow inputs carry an origin hint, run or panel

The Inputs editor and the canvas label show it, and the run forms leave
panel inputs out.
This commit is contained in:
2026-10-15 10:08:46 +02:00
parent 43498037be
commit e85b18092f
10 changed files with 111 additions and 39 deletions
+9
View File
@@ -267,6 +267,15 @@ class FlowInput(BaseModel):
spec: MessageSpec
initial: Any | None = None
origin: Literal["run", "panel"] = Field(
default="run",
description=(
"What writes it. A run input is a parameter a run passes in, a "
"panel input is written by a dashboard or panel control. A hint "
"only: run forms leave panel inputs out, the engine treats both "
"the same."
),
)
class FlowDef(BaseModel):
+6 -1
View File
@@ -96,13 +96,18 @@ Declare messages supplied by dashboard controls, the API or run parameters as
the flow's **Inputs**. Give live inputs a starting value:
```json
{"inputs": [{"spec": {"name": "setpoint", "dtype": "float"}, "initial": 21.0}]}
{"inputs": [{"spec": {"name": "setpoint", "dtype": "float"}, "initial": 21.0, "origin": "panel"}]}
```
This seeds `house.setpoint` in flow `house` if it has no value yet. Later
publications replace it. Batch inputs are run parameters, and batch outputs
select the run's result. See [Where a node's values come from](values.md).
`origin` says what writes an input: `run` (the default) for a parameter a run
passes in, `panel` for a dashboard or panel control. The canvas labels each
input with it, and the run form leaves panel inputs out, so a run keeps their
starting value. The engine treats both the same.
## Drafts and publishing
- **Saving** stores a draft; the engine keeps running the published version.
+4 -2
View File
@@ -47,8 +47,10 @@ The initial value seeds an unset live message. Later publications take over;
changing the starting value does not overwrite a value already in state.
Use **Publish this value now** in the input row to send it immediately.
The canvas draws flow inputs as labelled endpoints. Without a producer or
declared input, a consuming node reports `unconnected_input`.
The canvas draws flow inputs as labelled endpoints, marked `run` or `panel`
by what writes them. A panel input is one a dashboard or panel control writes,
and the run form does not ask for it. Without a producer or declared input, a
consuming node reports `unconnected_input`.
A batch flow's inputs are run parameters. Its **Result** section selects the
messages returned when the run finishes. See [Runs](runs.md).
+1 -1
View File
@@ -80,7 +80,7 @@ Open **Flow settings** from the pencil button in the dock.
|---|---|
| **Running** | starts or stops the published live flow's subscriptions, schedules and webhooks |
| **Mode** | chooses continuous `live` operation or requested `batch` runs |
| **Inputs** | declares external messages and starting values; batch inputs become run parameters |
| **Inputs** | declares external messages, their starting values and whether a run or a panel writes them; run inputs become run parameters |
| **Result** | selects batch output messages |
| **Contents** | shows the node count and message namespace |
+14
View File
@@ -1355,6 +1355,13 @@ export const FlowInput_InputSchema = {
}
],
title: 'Initial'
},
origin: {
type: 'string',
enum: ['run', 'panel'],
title: 'Origin',
description: 'What writes it. A run input is a parameter a run passes in, a panel input is written by a dashboard or panel control. A hint only: run forms leave panel inputs out, the engine treats both the same.',
default: 'run'
}
},
type: 'object',
@@ -1376,6 +1383,13 @@ export const FlowInput_OutputSchema = {
}
],
title: 'Initial'
},
origin: {
type: 'string',
enum: ['run', 'panel'],
title: 'Origin',
description: 'What writes it. A run input is a parameter a run passes in, a panel input is written by a dashboard or panel control. A hint only: run forms leave panel inputs out, the engine treats both the same.',
default: 'run'
}
},
type: 'object',
+13
View File
@@ -391,14 +391,27 @@ export type FlowDetail = {
export type FlowInput_Input = {
spec: MessageSpec;
initial?: (unknown | null);
/**
* What writes it. A run input is a parameter a run passes in, a panel input is written by a dashboard or panel control. A hint only: run forms leave panel inputs out, the engine treats both the same.
*/
origin?: 'run' | 'panel';
};
/**
* What writes it. A run input is a parameter a run passes in, a panel input is written by a dashboard or panel control. A hint only: run forms leave panel inputs out, the engine treats both the same.
*/
export type origin = 'run' | 'panel';
/**
* A message the flow starts with rather than computes.
*/
export type FlowInput_Output = {
spec: MessageSpec;
initial?: (unknown | null);
/**
* What writes it. A run input is a parameter a run passes in, a panel input is written by a dashboard or panel control. A hint only: run forms leave panel inputs out, the engine treats both the same.
*/
origin?: 'run' | 'panel';
};
/**
+48 -30
View File
@@ -6,6 +6,7 @@ import { MessagesService } from "@/client"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Segmented } from "@/components/ui/segmented"
import {
Select,
SelectContent,
@@ -160,6 +161,12 @@ function usePublishInput() {
})
}
/** What writes an input: a run passing it in, or a control on a panel. */
const ORIGINS = [
["run", "run"],
["panel", "panel"],
] as const
function InputRow({
flow,
declared,
@@ -231,34 +238,44 @@ function InputRow({
</Button>
</div>
{live && name ? (
<div className="flex items-center gap-2 pl-1 text-xs text-muted-foreground">
<span className="shrink-0">now</span>
{current === undefined ? (
<span className="flex-1">—</span>
) : (
<span className="min-w-0 flex-1">
<ValuePreview value={current.value} dtype={spec.dtype} />
</span>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
aria-label="Publish this value now"
disabled={publish.isPending || declared.initial === null}
onClick={() =>
publish.mutate({ message, value: declared.initial })
}
>
<ArrowUpFromLine />
</Button>
</TooltipTrigger>
<TooltipContent>Put this value into the flow now</TooltipContent>
</Tooltip>
</div>
) : null}
<div className="flex items-center gap-2 pl-1 text-xs text-muted-foreground">
<Segmented
label="Written by"
value={declared.origin ?? "run"}
options={ORIGINS}
onChange={(origin) =>
onChange({ ...declared, origin: origin as typeof declared.origin })
}
/>
{live && name ? (
<>
<span className="shrink-0">now</span>
{current === undefined ? (
<span className="flex-1">—</span>
) : (
<span className="min-w-0 flex-1">
<ValuePreview value={current.value} dtype={spec.dtype} />
</span>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
aria-label="Publish this value now"
disabled={publish.isPending || declared.initial === null}
onClick={() =>
publish.mutate({ message, value: declared.initial })
}
>
<ArrowUpFromLine />
</Button>
</TooltipTrigger>
<TooltipContent>Put this value into the flow now</TooltipContent>
</Tooltip>
</>
) : null}
</div>
</div>
)
}
@@ -338,8 +355,9 @@ export function BoundarySections({
<div className="flex items-center justify-between">
<span className={PANEL_SECTION}>Inputs</span>
<InfoTip label="Inputs">
Messages that arrive from outside, such as a dashboard control, a
run or the API, and the value the flow starts from.
Messages that arrive from outside, and the value the flow starts
from. A run passes a run input in, a dashboard or panel control
writes a panel input.
</InfoTip>
<Button
variant="ghost"
+9 -2
View File
@@ -94,9 +94,16 @@ export function initialText(inputs: FlowInput_Input[]): Record<string, string> {
)
}
/** The declared inputs worth drawing a field for. */
/**
* The declared inputs worth drawing a field for.
*
* A panel input is a control's to write, so a run keeps its starting value
* and the form does not offer it as a parameter.
*/
export function declaredInputs(definition: FlowDef_Input): FlowInput_Input[] {
return (definition.inputs ?? []).filter((one) => one.spec?.name)
return (definition.inputs ?? []).filter(
(one) => one.spec?.name && one.origin !== "panel",
)
}
/**
+5 -2
View File
@@ -91,11 +91,14 @@ export function deriveEdges(nodes: NodeDef_Input[], flow: string): Edge[] {
*/
export function boundaryKey(doc: {
mode?: string | null
inputs?: { spec?: MessageSpec | null }[] | null
inputs?: { spec?: MessageSpec | null; origin?: string }[] | null
outputs?: string[] | null
}): string {
const inputs = (doc.inputs ?? [])
.map((one) => `${one.spec?.name ?? ""}:${one.spec?.dtype ?? ""}`)
.map(
(one) =>
`${one.spec?.name ?? ""}:${one.spec?.dtype ?? ""}:${one.origin ?? ""}`,
)
.join(",")
return `${doc.mode ?? "live"}|${inputs}|${(doc.outputs ?? []).join(",")}`
}
+2 -1
View File
@@ -62,7 +62,8 @@ function boundaryOf(
kind: "input",
id: `input:${message}`,
label: displayName(flow, message),
detail: declared.spec?.dtype ?? "float",
// What writes it, so a control's input does not read as a run's.
detail: `${declared.origin ?? "run"} · ${declared.spec?.dtype ?? "float"}`,
provides: [message],
requires: [],
})