Help us shape the future of Go! Take the 2026 Go Developer Survey

flow

package
v0.65.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package flow implements the flows/*.yaml config schema for the desktop pipeline's Node-RED-style graph: a strict decoder, a per-node-type registry, cross-file reference validation, and directory loaders.

A flow is a directed graph of Node values connected by Wire edges. The flow's id is not stored in the file — it is the filename stem (e.g. "triage.yaml" -> id "triage") so the file and its id can never disagree.

This package is deliberately self-contained: it does not know about Wails, the desktop pipeline database, or cmd/desktop/internal/app/sources/github/feed. Cross-file action lookups are supplied by the caller through the Refs interface, so actions can stay owned by their own package without this package depending on them.

Index

Constants

View Source
const (

	// DefaultFunctionTimeout is used when timeout is omitted.
	DefaultFunctionTimeout = 5 * time.Second
)
View Source
const DefaultResurfacePolicy = ResurfacePolicyStateChanges
View Source
const NodeTypeFeed = "feed"

NodeTypeFeed is the feed terminal's type discriminator. It is named because a feed node is the only node type addressed from outside the graph: its id, qualified by the flow's, is the feed id inbox membership is keyed on.

View Source
const NotifyCooldownDefault = 5 * time.Minute

NotifyCooldownDefault is the per-item delivery floor a notify node uses when it declares no cooldown of its own: once a node interrupts about an item, it stays quiet about that same item for this long.

View Source
const NotifySeverityDefault = "info"

NotifySeverityDefault is the severity a notify node uses when it declares none: an ordinary, non-interrupting banner.

Variables

View Source
var (
	ErrFlowNotFound         = errors.New("flow: flow not found")
	ErrNodeNotFound         = errors.New("flow: node not found")
	ErrNodeNotImageMarkable = errors.New("flow: node does not carry an image mark")
)

CategoryOrder is the presentation order for the three categories: a flow reads source to destination, and so does its documentation.

View Source
var ErrInvalidResurfacePolicy = fmt.Errorf("not a valid ResurfacePolicy, try [%s]", strings.Join(_ResurfacePolicyNames, ", "))
View Source
var WorkedExampleYAML string

WorkedExampleYAML is the canonical sample flow: a sources.github feeding a github-filter, a two-output function node, and both terminal kinds (sources.github -> github-filter -> function -> {feed, action}).

It has exactly one home because it has two jobs that must never disagree. TestLoadFlow_WorkedExample loads these bytes and asserts they parse and validate, and cmd/desktop/internal/app/prompts embeds them in the flows authoring prompt as the concrete example an agent works from. A prompt example that no longer parses is a broken prompt, so the loader test is what keeps it honest.

Functions

func IsFlowFile added in v0.62.0

func IsFlowFile(path string) bool

IsFlowFile reports whether a change to path needs a reload. It keeps .ui.yaml, unlike isFlowDefinition, and excludes .sidebar.yaml: the frontend owns that state and applies it optimistically, so a reload on its writes would flash the sidebar.

func MigrateDir

func MigrateDir(dir, backupDir string, log *zerolog.Logger) error

MigrateDir migrates every flow definition file under dir forward to current, writing a backup under backupDir for each rewrite. Skips .ui.yaml/.sidebar.yaml siblings via isFlowDefinition (the same filter LoadFlows uses). Per-file errors are logged, not fatal — matching flow's per-file-isolation semantics.

func NodeDoc

func NodeDoc(nodeType string) (string, error)

NodeDoc returns the markdown documentation for a registered node type.

func NodeTypes

func NodeTypes() []string

NodeTypes returns every registered node type in sorted order. Sorting keeps prompt output byte-stable across runs — Go map iteration is randomized.

func ResurfacePolicyNames

func ResurfacePolicyNames() []string

ResurfacePolicyNames returns a list of possible string values of ResurfacePolicy.

func SaveFlow

func SaveFlow(path string, f Flow) error

SaveFlow writes f to path (flows/<id>.yaml). The frontend never serializes YAML itself: it sends a Flow value and this is the only place that turns it into a document, matching feed's one-way "Go owns the on-disk format" convention.

If path doesn't exist (or is empty), a clean document is marshaled with a short header comment. If it exists, its yaml.Node tree is edited in place: version/name/enabled are set as scalars on the existing root mapping, and the nodes/wires sequences are replaced wholesale. This preserves the document's leading header comment and any comments on unrelated top-level keys, but — since there's no way to tell, from a Flow value alone, which individual node or wire actually changed — comments attached to specific node/wire entries do not survive an edit. If per-entry comments become important, SaveFlow will need a richer edit model; round-trip structural fidelity (LoadFlow -> SaveFlow -> LoadFlow yields an equal Flow) does not depend on it.

func SaveSidebar

func SaveSidebar(path string, layout SidebarLayout) error

SaveSidebar writes a flow's sidebar layout atomically. Like SaveUI it always marshals a clean document: sidebar-layout files are machine-written only and carry no hand-authored comments worth preserving.

func SaveUI

func SaveUI(path string, layout Layout) error

SaveUI writes a flow's layout atomically. Unlike SaveFlow, this always marshals a clean document: layout files are machine-written only and carry no hand-authored comments worth preserving.

Types

type ActionConfig

type ActionConfig struct {
	Action string `json:"action" yaml:"action"`
}

ActionConfig is an action node: 1 input, 0 outputs (terminal). It enqueues an output_command against the referenced desktop actions.yml action id.

func (*ActionConfig) Inputs

func (c *ActionConfig) Inputs() int

func (*ActionConfig) Outputs

func (c *ActionConfig) Outputs() int

func (*ActionConfig) Validate

func (c *ActionConfig) Validate(refs Refs) error

type FeedConfig

type FeedConfig struct {
	Icon        string `json:"icon,omitempty"        yaml:"icon,omitempty"`
	Description string `json:"description,omitempty" yaml:"description,omitempty"`
}

FeedConfig is a feed node: 1 input, 0 outputs (terminal). The node *is* the feed — its identity is the node id (flow-qualified as "<flowId>/<nodeId>" for membership claims). Icon and Description are purely cosmetic sidebar presentation: the icon shown in the tree and the tooltip surfaced on hover (handy context for LLM-generated feeds). Both are optional.

func (*FeedConfig) Inputs

func (c *FeedConfig) Inputs() int

func (*FeedConfig) Outputs

func (c *FeedConfig) Outputs() int

func (*FeedConfig) Validate

func (c *FeedConfig) Validate(Refs) error

type Flow

type Flow struct {
	// ID is the filename stem (no extension), never a value read from the
	// file itself.
	ID        string          `json:"id"`
	Name      string          `json:"name"`
	Enabled   bool            `json:"enabled"`
	Resurface ResurfacePolicy `json:"resurface,omitempty"`
	// Image is the content hash of the profile's avatar, whose normalized PNG
	// lives in the app data dir (see cmd/desktop/internal/app/profileimg). It is owned by
	// SetImage, not the graph editor: FlowStore.Save preserves whatever is on
	// disk so a graph save never drops it. Empty means "no avatar" — the rail
	// falls back to the letter chip.
	Image string `json:"image,omitempty"`
	Nodes []Node `json:"nodes"`
	Wires []Wire `json:"wires"`
}

func LoadFlow

func LoadFlow(path string, refs Refs) (Flow, []string, error)

LoadFlow parses and validates a single flows/*.yaml file. The flow's id is the filename stem (extension stripped), never a value read from the file. On success it returns the Flow plus any soft warnings; a hard validation or parse failure returns a non-nil error and a zero Flow.

func LoadFlows

func LoadFlows(dir string, refs Refs) (flows []Flow, perFileErrors map[string]error, warnings map[string][]string)

LoadFlows loads every *.yaml/*.yml file directly inside dir, except a flow's sibling <id>.ui.yaml layout file (see SaveUI/LoadUI) — layouts live in the same directory but are never flow definitions. Each file is isolated: a broken flow is recorded in perFileErrors (keyed by filename) and skipped, while the remaining files still load. warnings is keyed by flow id and only holds entries for flows that produced soft warnings.

func ParseDocument

func ParseDocument(id string, data []byte, refs Refs) (Flow, []string, error)

ParseDocument parses and validates a flow document that is not on disk, under the id the caller gives it — a dry run against an unsaved edit, say. It is the same migrate-then-decode path LoadFlow uses, so a document that parses here is one that would load from a file, and one that does not fails with the message the file would have produced.

JSON is accepted too: the decoder is YAML, of which JSON is a subset, so an API caller can send the document as a JSON object rather than as text.

func (Flow) FeedID

func (f Flow) FeedID(nodeID string) string

FeedID is the id inbox membership uses for one of this flow's feed nodes: the node id qualified by the flow's, which is what makes it unique across profiles that reuse a node id.

func (Flow) FeedNodes

func (f Flow) FeedNodes() []Node

FeedNodes returns the flow's feed terminals in declaration order. A feed exists because a node declares it, not because anything has landed in it, so this is the authoritative list — inbox counts are a join onto it.

type FlowStatus

type FlowStatus struct {
	ID       string
	Flow     Flow
	Valid    bool
	Err      error
	Warnings []string
}

FlowStatus is one flows/*.yaml file's load outcome, keyed by the flow id (the filename stem — see LoadFlow). Valid flows carry Flow and any soft Warnings; invalid files carry Err instead and a zero Flow, so a listing UI can show a broken flow file (and why) rather than have it silently vanish from the list.

type FlowStore

type FlowStore struct {
	// contains filtered or unexported fields
}

FlowStore holds the flows loaded from a flows/*.yaml directory, reloading on demand (Reload) or from a file watcher. It is the backend half of Deploy: a flows-dir change reaches here via Reload, and the app emits "flows:updated" so the frontend knows to re-fetch and reconcile its running graph snapshots. This store only ever swaps its own in-memory snapshot; it does not touch a running graph.

Thread-safe: every method takes the same mutex.

func NewFlowStore

func NewFlowStore(dir string, refs Refs) *FlowStore

NewFlowStore returns a store over dir (typically desktop.FlowsDir()), resolving action-node references through refs. Nothing is read from disk until the first List/Get/Save/Statuses call.

func (*FlowStore) Create

func (s *FlowStore) Create(name string, seed Seed) (Flow, error)

Create writes a new flow (a new "profile") named name: it slugifies name to a flow id unique among existing flows, writes seed's graph plus its layout, reloads, and returns the loaded flow. A profile is a flow, so this is how the app's "New profile" affordance is backed.

An empty seed writes an empty graph, which is what a workspace created before any account is connected gets: a source node carries a credential ref, so there is no unconfigured source node to stand in for one.

func (*FlowStore) Delete

func (s *FlowStore) Delete(id string) error

Delete removes a flow (a "profile") and its sibling layout file, then reloads. A missing flow file is not an error — the end state (gone) is the same either way.

func (*FlowStore) Exists

func (s *FlowStore) Exists(id string) bool

Exists reports whether a flow file backs id, parsing or not. It is a weaker question than Get's: a file that fails to validate has no Flow to return but is very much there, and deleting or renaming it is how that gets fixed.

func (*FlowStore) Get

func (s *FlowStore) Get(id string) (Flow, bool)

Get returns one loaded flow by id.

func (*FlowStore) GetLayout

func (s *FlowStore) GetLayout(id string) Layout

GetLayout returns id's node layout — see LoadUI for missing/broken-file semantics (an empty Layout, never an error).

func (*FlowStore) GetSidebar

func (s *FlowStore) GetSidebar(id string) SidebarLayout

GetSidebar returns id's sidebar layout (feed folders + ordering) — see LoadSidebar for missing/broken-file semantics (an empty layout, never an error).

func (*FlowStore) List

func (s *FlowStore) List() []Flow

List returns every successfully loaded flow in rail order — see SetOrder. Flows whose file failed to load are omitted — see Statuses for the full picture, including broken files.

func (*FlowStore) ParseDocument

func (s *FlowStore) ParseDocument(id string, data []byte) (Flow, []string, error)

ParseDocument parses a flow document that is not on disk, resolving its references the same way a loaded file's are. Nothing is written and the store's own snapshot is untouched — this is how an unsaved edit is checked, or executed by a dry run, without being deployed first.

func (*FlowStore) Reload

func (s *FlowStore) Reload() error

Reload re-reads every flow file in the directory. It only returns an error when the directory itself can't be created/read (rare — the flows dir is created on demand); a broken individual flow file is never a Reload error, it just shows up in Statuses/Errors.

func (*FlowStore) Rename

func (s *FlowStore) Rename(id, name string) (Flow, error)

Rename updates a flow's display name without changing its stable id or any graph content.

func (*FlowStore) Save

func (s *FlowStore) Save(f Flow) error

Save re-validates f (the same checks LoadFlow runs, via the store's Refs) and, only if that passes, writes it with SaveFlow and reloads. An invalid flow is rejected before anything is written, so the file on disk — and the store's in-memory state — is untouched: the last-good flow keeps serving.

func (*FlowStore) SaveLayout

func (s *FlowStore) SaveLayout(id string, layout Layout) error

SaveLayout persists id's node layout.

func (*FlowStore) SaveSidebar

func (s *FlowStore) SaveSidebar(id string, layout SidebarLayout) error

SaveSidebar persists id's sidebar layout.

func (*FlowStore) SetEnabled

func (s *FlowStore) SetEnabled(id string, enabled bool) (Flow, error)

SetEnabled updates whether a flow participates in polling and runtime execution without changing its graph or display name.

func (*FlowStore) SetImage

func (s *FlowStore) SetImage(id, hash string) (Flow, error)

SetImage sets a flow's avatar reference — the content hash of its stored image, or "" to clear it — without touching its graph or other fields. Like SetEnabled it reads the file directly rather than the possibly-stale cached snapshot, so a concurrent external graph edit is not reverted.

func (*FlowStore) SetOrder

func (s *FlowStore) SetOrder(ids []string)

SetOrder sets the rail order: the ids given sort first, in the order given, and every other flow sorts alphabetically after them. An id naming no flow is ignored rather than rejected, so deleting a profile never invalidates the setting that named it, and an id the caller repeats keeps its first position.

Order is process state, not flow state: it comes from settings.yaml, which this package does not read, and a reload of the flows directory leaves it alone.

func (*FlowStore) SetSourceImage

func (s *FlowStore) SetSourceImage(flowID, nodeID, hash string) (Flow, error)

SetSourceImage sets (hash != "") or clears (hash == "") source node nodeID's feed-mark image in flow flowID, saves the flow, and returns the reloaded flow. It reads from disk rather than the cached snapshot, so a concurrent external edit is not reverted.

func (*FlowStore) SourceImageRef

func (s *FlowStore) SourceImageRef(flowID, nodeID string) (string, error)

SourceImageRef returns source node nodeID's feed-mark image hash, or "" when it has none.

func (*FlowStore) Statuses

func (s *FlowStore) Statuses() []FlowStatus

Statuses returns one FlowStatus per flow file in the directory — valid and invalid alike — in rail order, for a listing UI that must surface broken flows too, not just the ones that loaded. A broken file sorts by the same rule a working one does: ordering the rail by one comparator and its error entries by another would interleave them unpredictably.

type FunctionConfig

type FunctionConfig struct {
	OnMessage string            `json:"on_message"        yaml:"on_message"`
	OutputsN  int               `json:"outputs,omitempty" yaml:"outputs,omitempty"`
	Timeout   duration.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
}

FunctionConfig is a function node: 1 input, N outputs (default 1). It carries the JavaScript the engine evaluates for each arriving message; this package only parses and validates the config.

on_message is the node's whole lifecycle. There are deliberately no start or stop hooks: the node does no I/O, so a stop hook could only mutate state that is about to be discarded, and a start hook is better written as lazy initialisation inside on_message (`state.counts ??= {}`). Having exactly one entry point also removes the pretence that `msg` may be undefined.

func (*FunctionConfig) EffectiveTimeout

func (c *FunctionConfig) EffectiveTimeout() time.Duration

EffectiveTimeout returns the configured timeout, defaulting to DefaultFunctionTimeout when unset.

func (*FunctionConfig) Inputs

func (c *FunctionConfig) Inputs() int

func (*FunctionConfig) Outputs

func (c *FunctionConfig) Outputs() int

Outputs returns the configured output count, defaulting to 1 when unset.

func (*FunctionConfig) Validate

func (c *FunctionConfig) Validate(Refs) error

type GithubFilterConfig

type GithubFilterConfig struct {
	Repos          []string `json:"repos,omitempty"           yaml:"repos,omitempty"`
	ExcludeRepos   []string `json:"exclude_repos,omitempty"   yaml:"exclude_repos,omitempty"`
	Authors        []string `json:"authors,omitempty"         yaml:"authors,omitempty"`
	ExcludeAuthors []string `json:"exclude_authors,omitempty" yaml:"exclude_authors,omitempty"`
	Labels         []string `json:"labels,omitempty"          yaml:"labels,omitempty"`
	ExcludeLabels  []string `json:"exclude_labels,omitempty"  yaml:"exclude_labels,omitempty"`
	Types          []string `json:"types,omitempty"           yaml:"types,omitempty"`
	Reasons        []string `json:"reasons,omitempty"         yaml:"reasons,omitempty"`
	CI             []string `json:"ci,omitempty"              yaml:"ci,omitempty"`
	Review         []string `json:"review,omitempty"          yaml:"review,omitempty"`
}

GithubFilterConfig is a github-filter node: 1 input, 2 outputs (port 0 = pass, port 1 = fail). Its fields mirror the profiles feed FilterDef: groups AND together, values within a group OR, and exclude groups win over includes.

func (*GithubFilterConfig) Inputs

func (c *GithubFilterConfig) Inputs() int

func (*GithubFilterConfig) Outputs

func (c *GithubFilterConfig) Outputs() int

func (*GithubFilterConfig) Validate

func (c *GithubFilterConfig) Validate(Refs) error

type HeadlessActionRefs

type HeadlessActionRefs interface{ ActionHeadlessCapable(string) bool }

HeadlessActionRefs is optionally implemented by action resolvers. Older resolvers still validate existence; desktop's resolver additionally blocks interactive launch actions from background flow nodes.

type LaunchChatConfig

type LaunchChatConfig struct {
	// Workspace is a directory name under the workspace root.
	Workspace string `json:"workspace" yaml:"workspace"`
	Prompt    string `json:"prompt"    yaml:"prompt"`
}

LaunchChatConfig is a terminal node that opens an agent workspace chat per item, with the rendered prompt as the first message.

func (*LaunchChatConfig) Inputs

func (c *LaunchChatConfig) Inputs() int

func (*LaunchChatConfig) Outputs

func (c *LaunchChatConfig) Outputs() int

func (*LaunchChatConfig) Validate

func (c *LaunchChatConfig) Validate(Refs) error

type LaunchSessionConfig

type LaunchSessionConfig struct {
	Repo  string `json:"repo"            yaml:"repo"`
	Agent string `json:"agent,omitempty" yaml:"agent,omitempty"`
	// Not `name`: that key is the node's own label.
	SessionName string `json:"sessionName,omitempty" yaml:"sessionName,omitempty"`
	Prompt      string `json:"prompt"                yaml:"prompt"`
}

LaunchSessionConfig is a terminal node that starts a hive session per item, rendering the same templates as an actions.yml launch-session action.

func (*LaunchSessionConfig) Inputs

func (c *LaunchSessionConfig) Inputs() int

func (*LaunchSessionConfig) Outputs

func (c *LaunchSessionConfig) Outputs() int

func (*LaunchSessionConfig) Validate

func (c *LaunchSessionConfig) Validate(Refs) error

type Layout

type Layout struct {
	Nodes map[string]NodePosition `json:"nodes" yaml:"nodes"`
}

Layout is the machine-written, non-authoritative sibling of a flow — and the on-disk shape of a <id>.ui.yaml file: node canvas positions only, keyed by node id. It is never consulted by LoadFlow/validation and never blocks a flow from loading — a missing or broken .ui.yaml just means the editor lays out nodes fresh (see LoadUI).

func LoadUI

func LoadUI(path string) Layout

LoadUI reads a flow's sibling layout file. A missing file, or one that fails to parse, is not an error — Layout is purely cosmetic — it returns an empty Layout instead so the caller always has something to range over.

type Node

type Node struct {
	ID       string     `json:"id"`
	Type     string     `json:"type"`
	Name     string     `json:"name,omitempty"`
	Disabled bool       `json:"disabled,omitempty"`
	Config   NodeConfig `json:"-"`
}

Node is one node in a flow's graph: the common envelope fields (id, type, name, disabled) plus a per-type Config decoded via the registry. The json tags document the wire shape produced by MarshalJSON/UnmarshalJSON (see node_json.go) — Node's own (Un)MarshalJSON/(Un)MarshalYAML methods bypass normal struct-tag reflection, so these tags are documentation, not decode wiring.

func (Node) MarshalJSON

func (n Node) MarshalJSON() ([]byte, error)

MarshalJSON flattens the envelope fields and the per-type config's own fields into one JSON object — the wire shape the desktop frontend's graph editor consumes over Wails (FlowsService.GetFlow/SaveFlow) — mirroring the flattened *.yaml on-disk shape produced by MarshalYAML.

func (Node) MarshalYAML

func (n Node) MarshalYAML() (any, error)

MarshalYAML implements yaml.Marshaler, inverting UnmarshalYAML's two-pass decode: the envelope fields (id, type, name, disabled) and the per-type config's own fields are flattened into one mapping, matching the on-disk shape UnmarshalYAML reads back (see the worked example in loader_test.go). Returning a *yaml.Node here (rather than a plain value) is a documented yaml.v3 pattern for building the node by hand: the encoder uses it directly instead of re-marshaling it through reflection.

func (*Node) UnmarshalJSON

func (n *Node) UnmarshalJSON(data []byte) error

UnmarshalJSON implements the same two-pass strict decode as UnmarshalYAML: (1) decode the envelope to read the `type` discriminator, (2) look up the type in the registry (unknown type is a hard error), (3) strict-decode the remaining fields — with the reserved envelope keys stripped out first — into a fresh per-type config, so an unrecognized per-type field is also a hard error (matching UnmarshalYAML's KnownFields(true) behavior).

func (*Node) UnmarshalYAML

func (n *Node) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML implements the two-pass strict decode: (1) decode a lax Header to read the `type:` discriminator, (2) look up the type in the registry (unknown type is a hard error), (3) strict-decode the node's remaining fields — with the reserved envelope keys stripped out first — into a fresh per-type config so an unknown per-type field is also a hard error.

type NodeCategory

type NodeCategory string

NodeCategory groups node types the way the palette and the flows prompt present them. It is derived from a type's port counts rather than declared, so it cannot disagree with what the graph validator enforces.

const (
	// CategorySources covers node types with no inputs — where a flow starts.
	CategorySources NodeCategory = "Sources"
	// CategoryProcess covers node types with both inputs and outputs.
	CategoryProcess NodeCategory = "Process"
	// CategoryDestinations covers terminal node types with no outputs.
	CategoryDestinations NodeCategory = "Destinations"
)

func CategoryOf

func CategoryOf(nodeType string) (NodeCategory, error)

CategoryOf classifies a registered node type from the port counts its config reports: no inputs makes it a source, no outputs a destination, anything else a processor.

type NodeConfig

type NodeConfig interface {
	Inputs() int
	Outputs() int
	Validate(refs Refs) error
}

NodeConfig is the per-type union every registered node type implements. Inputs/Outputs report the node's port counts (used by wire validation); Validate checks the node's own fields, including any cross-file references, against the supplied Refs resolver.

type NodePosition

type NodePosition struct {
	X int `json:"x" yaml:"x"`
	Y int `json:"y" yaml:"y"`
}

NodePosition is one node's canvas coordinates in a flow's sibling <id>.ui.yaml layout file.

type NotifyConfig

type NotifyConfig struct {
	// Title renders the notification's headline. Required — a native
	// notification without a title is rejected by the OS layer.
	Title string `json:"title" yaml:"title"`
	// Body renders the notification's message. Optional.
	Body string `json:"body,omitempty" yaml:"body,omitempty"`
	// Severity selects the native interruption level (see notifySeverities).
	// Empty means NotifySeverityDefault.
	Severity string `json:"severity,omitempty" yaml:"severity,omitempty"`
	// Sound is a pointer so an absent key ("use the default", which is sound
	// on) is distinguishable from an explicit `sound: false`. It can only
	// silence a notification — the global notification-sound setting still
	// wins when it is off. Resolve through SoundOrDefault.
	Sound *bool `json:"sound,omitempty" yaml:"sound,omitempty"`
	// CooldownSeconds is a per-item delivery floor: once this node interrupts
	// about an item, it stays quiet about that same item for this long. A
	// pointer so an absent key ("use NotifyCooldownDefault") is distinct from
	// an explicit 0 ("no cooldown; every accepted message may interrupt").
	// This is a delivery floor, not dedup — deciding *whether* an item is
	// worth notifying is upstream's job. Resolve through CooldownOrDefault.
	CooldownSeconds *int `json:"cooldownSeconds,omitempty" yaml:"cooldownSeconds,omitempty"`
}

NotifyConfig is a notify node: 1 input, 0 outputs (terminal). Every arriving message enqueues an output_command that the notify executor delivers as a native OS notification, with Title/Body rendered as Go text/templates over the message (the same templating story as an action's prompt_template — see cmd/desktop/internal/app/actions/launch_session.go).

The node cannot override the app's notification settings: the executor checks the global kill switch before every delivery, so "notifications off" always wins over any flow.

func (*NotifyConfig) CooldownOrDefault

func (c *NotifyConfig) CooldownOrDefault() time.Duration

CooldownOrDefault resolves CooldownSeconds, defaulting to NotifyCooldownDefault when the key is absent. An explicit 0 disables the cooldown entirely.

func (*NotifyConfig) Inputs

func (c *NotifyConfig) Inputs() int

func (*NotifyConfig) NotifyDeclaration

func (c *NotifyConfig) NotifyDeclaration() (cfg *NotifyConfig, ok bool)

NotifyDeclaration satisfies dispatch's notify-raiser capability: a notify node always has content to raise — it notifies for whatever is routed to it, which is the author's explicit choice in authoring the node at all.

func (*NotifyConfig) Outputs

func (c *NotifyConfig) Outputs() int

func (*NotifyConfig) SeverityOrDefault

func (c *NotifyConfig) SeverityOrDefault() string

SeverityOrDefault resolves Severity, defaulting to NotifySeverityDefault when the node declares none.

func (*NotifyConfig) SoundOrDefault

func (c *NotifyConfig) SoundOrDefault() bool

SoundOrDefault resolves Sound, defaulting to true (play the notification sound) when the key is absent.

func (*NotifyConfig) Validate

func (c *NotifyConfig) Validate(Refs) error

type Refs

type Refs interface {
	// ResolveAction reports whether id names a known action.
	ResolveAction(id string) bool
}

Refs resolves the one remaining cross-file reference a flow's nodes point at: the action node's desktop actions.yml action id. (Source and feed nodes are now self-contained — a source embeds its fetch config, a feed's identity is its node id — so they no longer resolve anything.) It is injected rather than imported so this package never depends on the actions loader.

type ResurfacePolicy

type ResurfacePolicy string

Flow is one parsed and validated flows/*.yaml document. Besides being the decode target for LoadFlow, it is the wire shape GetFlow/SaveFlow expose to the desktop frontend's graph editor over Wails — hence the json tags alongside the (unused-by-Flow-itself, since flowFile is the YAML decode target) documentation of the on-disk names. ENUM(all, state-changes, never)

const (
	// ResurfacePolicyAll is a ResurfacePolicy of type all.
	ResurfacePolicyAll ResurfacePolicy = "all"
	// ResurfacePolicyStateChanges is a ResurfacePolicy of type state-changes.
	ResurfacePolicyStateChanges ResurfacePolicy = "state-changes"
	// ResurfacePolicyNever is a ResurfacePolicy of type never.
	ResurfacePolicyNever ResurfacePolicy = "never"
)

func ParseResurfacePolicy

func ParseResurfacePolicy(name string) (ResurfacePolicy, error)

ParseResurfacePolicy attempts to convert a string to a ResurfacePolicy.

func (*ResurfacePolicy) AppendText

func (x *ResurfacePolicy) AppendText(b []byte) ([]byte, error)

AppendText appends the textual representation of itself to the end of b (allocating a larger slice if necessary) and returns the updated slice.

Implementations must not retain b, nor mutate any bytes within b[:len(b)].

func (ResurfacePolicy) IsValid

func (x ResurfacePolicy) IsValid() bool

IsValid provides a quick way to determine if the typed value is part of the allowed enumerated values

func (ResurfacePolicy) MarshalText

func (x ResurfacePolicy) MarshalText() ([]byte, error)

MarshalText implements the text marshaller method.

func (ResurfacePolicy) String

func (x ResurfacePolicy) String() string

String implements the Stringer interface.

func (*ResurfacePolicy) UnmarshalText

func (x *ResurfacePolicy) UnmarshalText(text []byte) error

UnmarshalText implements the text unmarshaller method.

type Seed

type Seed struct {
	Nodes  []Node
	Wires  []Wire
	Layout Layout
}

Seed is the graph a newly created flow starts with, plus the canvas positions for it. Create takes one from its caller rather than building it: a starter graph names source connectors, and this package is connector-neutral by design.

The zero Seed is an empty graph, which is a valid flow — a workspace is the thing that exists before any credential does.

type SidebarFolder

type SidebarFolder struct {
	ID    string   `json:"id"    yaml:"id"`
	Name  string   `json:"name"  yaml:"name"`
	Feeds []string `json:"feeds" yaml:"feeds"`
}

SidebarFolder is one named group of feeds in a profile's sidebar. Feeds lists the member feed node ids in display order. Note there is no collapsed field: whether a folder is expanded is transient view state the frontend keeps in localStorage, not configuration — this file records structure and order only.

type SidebarItem

type SidebarItem struct {
	Feed   string         `json:"feed,omitempty"   yaml:"feed,omitempty"`
	Folder *SidebarFolder `json:"folder,omitempty" yaml:"folder,omitempty"`
}

SidebarItem is one entry in the sidebar's ordered top level: either a bare feed (Feed set to a feed node id) or a folder (Folder set). Exactly one is meant to be non-empty; a malformed item with neither is ignored by the frontend when it reconciles the layout against the flow's actual feed nodes.

type SidebarLayout

type SidebarLayout struct {
	Items []SidebarItem `json:"items" yaml:"items"`
}

SidebarLayout is the machine-written, non-authoritative sibling of a flow — the on-disk shape of a <id>.sidebar.yaml file. It records how a profile's feed nodes are grouped into folders and ordered in the sidebar's FEEDS section. Like Layout (.ui.yaml) it is purely cosmetic and keyed by node id: a missing or broken file just means the sidebar falls back to listing feeds in flow-node order, feed nodes absent from the layout are appended (so a newly added feed is never hidden), and entries for feeds that no longer exist are dropped. It is never consulted by LoadFlow/validation and never blocks a flow from loading.

func LoadSidebar

func LoadSidebar(path string) SidebarLayout

LoadSidebar reads a flow's sibling sidebar-layout file. A missing file, or one that fails to parse, is not an error — the layout is purely cosmetic — it returns an empty SidebarLayout instead so the caller always has an (empty) slice to range over.

type SourceConfig

type SourceConfig struct {
	// contains filtered or unexported fields
}

SourceConfig is the node-graph half of a source connector: it adapts a connector.Config — which knows only its own fields and how to validate them — to the NodeConfig every node type implements.

The port counts live here rather than on the connector because they are graph vocabulary: a source is 0-in/1-out by definition, and a connector that had to declare it could get it wrong. Validate forwards, because a source node's config is self-contained; Refs resolves the action node's cross-file reference and a source has none.

func NewSourceConfig

func NewSourceConfig(connectorType string, cfg connector.Config) *SourceConfig

NewSourceConfig wraps a connector's parsed config as a node config.

func (*SourceConfig) Connector

func (c *SourceConfig) Connector() connector.Config

Connector returns the parsed connector config, which is what a factory needs to construct a running instance.

func (*SourceConfig) ConnectorType

func (c *SourceConfig) ConnectorType() string

ConnectorType is the registered connector this node configures — the same string as the node's `type:`.

func (*SourceConfig) Inputs

func (c *SourceConfig) Inputs() int

func (*SourceConfig) MarshalJSON

func (c *SourceConfig) MarshalJSON() ([]byte, error)

func (*SourceConfig) MarshalYAML

func (c *SourceConfig) MarshalYAML() (any, error)

MarshalYAML and MarshalJSON emit the connector's own fields, so a source node round-trips to exactly the flattened shape every other node type does — the wrapper is invisible on disk and on the wire.

func (*SourceConfig) Outputs

func (c *SourceConfig) Outputs() int

func (*SourceConfig) UnmarshalJSON

func (c *SourceConfig) UnmarshalJSON(data []byte) error

UnmarshalJSON mirrors UnmarshalYAML: json.Unmarshal on a type with a custom unmarshaler drops the outer decoder's DisallowUnknownFields, so it is re-applied here.

func (*SourceConfig) UnmarshalYAML

func (c *SourceConfig) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML re-creates the strict decode rather than delegating to the caller's decoder. A custom unmarshaler bypasses the outer decoder's KnownFields(true), so without this an unknown key in a source node's config would decode to nothing instead of failing the load — the silent-failure mode every other node type is protected from.

func (*SourceConfig) Validate

func (c *SourceConfig) Validate(Refs) error

type Wire

type Wire struct {
	From string `json:"from"          yaml:"from"`
	Out  int    `json:"out,omitempty" yaml:"out,omitempty"`
	To   string `json:"to"            yaml:"to"`
}

Wire is a directed edge from one node's output port to another node's (sole) input. Out defaults to 0 when omitted in YAML.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL