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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
2026-10-15 10:08:46 +02:00
co-authored by Claude Opus 5.5
parent ccc7081b08
commit f5be932df3
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: [],
})