workflow

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: AGPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package workflow implements a server-side, n8n-style workflow execution engine: a DAG of typed nodes (triggers, AI agents, logic, actions, data) wired by ports, executed with join-gated scheduling, cancellation, and per-node run state suitable for a live executions inspector.

The package is pure: it imports no other internal package. All side effects (running an agent, HTTP, filesystem, shell, persistence, event emission) are injected via Deps so the engine is unit-testable and reusable. The wire model (Workflow/WorkflowNode/WorkflowEdge/WorkflowNodeConfig) mirrors the Angular desktop `workflow.models.ts` byte-for-byte so definitions round-trip 1:1.

Index

Constants

View Source
const (
	TypeTriggerManual   = "trigger_manual"
	TypeTriggerSchedule = "trigger_schedule"
	TypeTriggerWebhook  = "trigger_webhook"
	TypeTriggerEvent    = "trigger_event"
	TypeTriggerForm     = "trigger_form"
	TypeAgent           = "agent"
	TypeAgentHandoff    = "agent_handoff"
	TypeCondition       = "condition"
	TypeSwitch          = "switch"
	TypeHTTPRequest     = "http_request"
	TypeCodeTransform   = "code_transform"
	TypeWorkspaceOp     = "workspace_op"
	TypeShellCommand    = "shell_command"
	TypeWait            = "wait"
	TypeLoop            = "loop"
	TypeMerge           = "merge"
	TypeSetVariable     = "set_variable"
	TypeEmailSend       = "email_send"
	TypeTriggerEmail    = "trigger_email"
	TypeSendChannel     = "send_channel"
	TypeLLMPrompt       = "llm_prompt"
	TypeWeb             = "web"
	TypeKnowledgeBase   = "knowledge_base"
	TypeSQLQuery        = "sql_query"
	TypeFilter          = "filter"
	TypeTemplate        = "template"
	TypeApproval        = "approval"
	TypeSubWorkflow     = "sub_workflow"
)

Node type identifiers (must match the TS WorkflowNodeType union).

View Source
const DefaultOutputPort = "output"

DefaultOutputPort is the port id the web UI assigns to a node's single default output. Standard nodes fire their default port as "" internally (see defaultPort in executor_nodes.go), so an edge saved from the UI's "output" port must be treated as that default when matching fired ports. Explicit branch ports (true/false/each/done/approved/rejected/default/case_*) carry their own ids on both sides and are returned unchanged.

Variables

This section is empty.

Functions

func IsTriggerType

func IsTriggerType(t string) bool

IsTriggerType reports whether a node type is a trigger (a run's seed node).

Types

type AgentReq

type AgentReq struct {
	NodeID    string
	AgentID   string
	Prompt    string
	Workspace string
}

type AgentResult

type AgentResult struct {
	Text      string
	SessionID string
}

type ApprovalReq

type ApprovalReq struct {
	NodeID    string
	Title     string
	Message   string
	Timeout   time.Duration
	OnTimeout string // "approve" | "reject"
}

type ChannelReq

type ChannelReq struct {
	NodeID    string
	ChannelID string
	Target    string
	Text      string
}

type Deps

type Deps struct {
	// RunAgent executes an AI agent node and returns its final text + session id.
	RunAgent func(ctx context.Context, req AgentReq) (AgentResult, error)
	// RunShell executes a shell command node.
	RunShell func(ctx context.Context, req ShellReq) (ShellResult, error)
	// SendEmail delivers an email_send node's message via the caller's SMTP sender
	// (e.g. the org's configured SMTP settings). When nil, email_send nodes error.
	SendEmail func(ctx context.Context, req EmailReq) error
	// SendChannel delivers a send_channel node's message through a configured
	// messaging channel (Slack/Telegram/Discord/WhatsApp). When nil, the node errors.
	SendChannel func(ctx context.Context, req ChannelReq) error
	// RunLLM performs a single, no-tools LLM completion for the llm_prompt node.
	RunLLM func(ctx context.Context, req LLMReq) (string, error)
	// WebSearch / WebFetch back the web node (search the web, fetch a URL as text).
	WebSearch func(ctx context.Context, query string, maxResults int) (string, error)
	WebFetch  func(ctx context.Context, url string) (string, error)
	// KBSearch / KBIngest back the knowledge_base node (vector search / ingest).
	KBSearch func(ctx context.Context, query string, topK int) (string, error)
	KBIngest func(ctx context.Context, req KBIngestReq) (string, error)
	// SQLQuery runs the sql_query node against an external database.
	SQLQuery func(ctx context.Context, req SQLReq) (SQLResult, error)
	// RunSubWorkflow runs another workflow to completion and returns its outputs
	// (sub_workflow node). The caller enforces a recursion-depth guard.
	RunSubWorkflow func(ctx context.Context, workflowID string, payload map[string]any) (map[string]any, error)
	// RequestApproval blocks on a human decision for the approval node.
	RequestApproval func(ctx context.Context, req ApprovalReq) (bool, error)
	// FS performs workspace-confined file operations.
	FS FSOps
	// HTTPClient is used by http_request nodes.
	HTTPClient *http.Client
	// RequestPerm asks the caller to approve a shell command (allow, allowAll).
	RequestPerm func(ctx context.Context, nodeID, command string) (allow, allowAll bool)
	// Emit publishes a run event to the bus (run_started/node_started/... ).
	Emit func(ev map[string]any)
	// PersistNode upserts a node run-state row (executions inspector).
	PersistNode func(ns NodeRunState)
	// Suspend durably records a long-wait suspension so a scheduler can resume the
	// run at resumeAt. When nil, wait nodes fall back to an in-process timer.
	Suspend func(nodeID string, resumeAt time.Time, snap Snapshot)
	// Workspace is the resolved filesystem root for fs/shell nodes.
	Workspace string
	// MaxConcurrency bounds parallel node execution per run (default 8).
	MaxConcurrency int
}

Deps injects all side effects the engine needs so the package imports no other internal package. It is constructed per-run by the caller (the selfhost handler) so RunAgent/RequestPerm/Emit close over the run id and event bus.

type EmailReq

type EmailReq struct {
	NodeID   string
	To       string
	Cc       []string
	Bcc      []string
	Subject  string
	Body     string
	HTML     bool
	FromName string
}

type FSOps

type FSOps interface {
	Read(path string) (string, error)
	Write(path, content string) error
	List(path string) ([]string, error)
	Search(query string) ([]string, error)
}

FSOps is the minimal workspace-confined filesystem surface for workspace_op.

type FormField

type FormField struct {
	Name     string `json:"name"`
	Type     string `json:"type,omitempty"`
	Required bool   `json:"required,omitempty"`
}

type KBIngestReq

type KBIngestReq struct {
	NodeID   string
	Name     string
	Content  string // when set, ingest this text directly
	FilePath string // otherwise, ingest a file at this workspace-relative path
}

type LLMReq

type LLMReq struct {
	NodeID string
	System string
	Prompt string
}

type NodeResult

type NodeResult struct {
	Output         any      // becomes $json for downstream nodes
	FiredPorts     []string // output ports to follow ("" = default)
	AgentSessionID string   // set by agent nodes for inspector deep-linking
	Suspend        bool     // wait node: run suspended, resume scheduled durably
}

NodeResult is what a node body produces.

type NodeRunState

type NodeRunState struct {
	RunID          string     `json:"runId"`
	NodeID         string     `json:"nodeId"`
	Status         NodeStatus `json:"status"`
	Input          any        `json:"input,omitempty"`
	Output         any        `json:"output,omitempty"`
	Error          string     `json:"error,omitempty"`
	AgentSessionID string     `json:"agentSessionId,omitempty"`
	Attempts       int        `json:"attempts,omitempty"`
	StartedAt      *time.Time `json:"startedAt,omitempty"`
	CompletedAt    *time.Time `json:"completedAt,omitempty"`
}

type NodeStatus

type NodeStatus string
const (
	NodePending NodeStatus = "pending"
	NodeRunning NodeStatus = "running"
	NodeDone    NodeStatus = "done"
	NodeError   NodeStatus = "error"
	NodeSkipped NodeStatus = "skipped"
	NodeWaiting NodeStatus = "waiting"
)

type RunOptions

type RunOptions struct {
	// TriggerNodeID, when set, seeds only that trigger node (the trigger that
	// started this run). Empty seeds all trigger/indegree-0 nodes.
	TriggerNodeID string
	// StartNodeID begins a partial ("run from here") execution at this node.
	StartNodeID string
	// SingleNode runs only StartNodeID and does not follow its output edges.
	SingleNode bool
	// SeedOutputs pre-populates upstream node outputs (partial execution) so the
	// start node's input can be assembled without re-running upstream nodes.
	SeedOutputs map[string]any
	// SeedVars pre-populates workflow variables (used when resuming a suspended run).
	SeedVars map[string]any
	// Resume marks this as the continuation of a suspended run; the wait node at
	// StartNodeID passes through instead of waiting again.
	Resume bool
	// Test honors the workflow's pinned data: a node with pinned output is not
	// executed; its pinned value is used instead.
	Test bool
}

RunOptions controls how a run is seeded (manual, trigger, or partial execution).

type RunStatus

type RunStatus string
const (
	RunPending  RunStatus = "pending"
	RunRunning  RunStatus = "running"
	RunDone     RunStatus = "done"
	RunError    RunStatus = "error"
	RunCanceled RunStatus = "canceled"
	// RunWaiting means the run suspended on a long wait node and will be resumed
	// by the scheduler at the persisted resume time (survives restarts).
	RunWaiting RunStatus = "waiting"
)

func Run

func Run(ctx context.Context, wf *Workflow, run *WorkflowRun, deps Deps, opts RunOptions) RunStatus

Run executes wf, mutating run (status, timing) and emitting events via deps. It blocks until the run finishes or ctx is cancelled, returning the final status.

type SQLReq

type SQLReq struct {
	NodeID string
	Driver string
	DSN    string
	Query  string
}

type SQLResult

type SQLResult struct {
	Rows     []map[string]any
	RowCount int
}

type ShellReq

type ShellReq struct {
	NodeID      string
	Command     string
	Cwd         string
	AutoApprove bool
}

type ShellResult

type ShellResult struct {
	Stdout   string
	Stderr   string
	ExitCode int
}

type Snapshot

type Snapshot struct {
	Outputs map[string]any `json:"outputs"`
	Vars    map[string]any `json:"vars"`
}

Snapshot captures a run's accumulated state at a suspend point so the run can be durably resumed from a wait node.

type SwitchCase

type SwitchCase struct {
	Expression string `json:"expression"`
	Label      string `json:"label,omitempty"`
	PortID     string `json:"portId"`
}

SwitchCase mirrors the desktop { expression, label, portId } shape. The engine evaluates Expression as a boolean; the first truthy case fires its PortID.

type Workflow

type Workflow struct {
	ID            string         `json:"id"`
	Name          string         `json:"name"`
	Description   string         `json:"description,omitempty"`
	WorkspacePath string         `json:"workspacePath,omitempty"`
	Nodes         []WorkflowNode `json:"nodes"`
	Edges         []WorkflowEdge `json:"edges"`
	Active        bool           `json:"active"`
	Tags          []string       `json:"tags,omitempty"`
	Variables     []WorkflowVar  `json:"variables,omitempty"`
	// PinnedData maps nodeID -> pinned output used during test runs.
	PinnedData map[string]any `json:"pinnedData,omitempty"`
	CreatedAt  time.Time      `json:"createdAt"`
	UpdatedAt  time.Time      `json:"updatedAt"`
}

type WorkflowEdge

type WorkflowEdge struct {
	ID           string `json:"id"`
	SourceNodeID string `json:"sourceNodeId"`
	SourcePort   string `json:"sourcePort,omitempty"`
	TargetNodeID string `json:"targetNodeId"`
	TargetPort   string `json:"targetPort,omitempty"`
	Label        string `json:"label,omitempty"`
	Style        string `json:"style,omitempty"`
}

type WorkflowNode

type WorkflowNode struct {
	ID       string             `json:"id"`
	Type     string             `json:"type"`
	Label    string             `json:"label,omitempty"`
	Sublabel string             `json:"sublabel,omitempty"`
	X        float64            `json:"x"`
	Y        float64            `json:"y"`
	Config   WorkflowNodeConfig `json:"config"`
	Disabled bool               `json:"disabled,omitempty"`
}

type WorkflowNodeConfig

type WorkflowNodeConfig struct {
	// agent / agent_handoff
	AgentID         string `json:"agentId,omitempty"`
	TaskPrompt      string `json:"taskPrompt,omitempty"`
	WorkspacePath   string `json:"workspacePath,omitempty"`
	WaitForComplete *bool  `json:"waitForCompletion,omitempty"`
	Timeout         int    `json:"timeout,omitempty"`
	// trigger_schedule
	Schedule string `json:"schedule,omitempty"`
	// trigger_webhook
	WebhookPath string `json:"webhookPath,omitempty"`
	// trigger_event
	EventType string `json:"eventType,omitempty"`
	// trigger_form
	FormFields []FormField `json:"formFields,omitempty"`
	// condition
	ConditionExpression string `json:"conditionExpression,omitempty"`
	TrueLabel           string `json:"trueLabel,omitempty"`
	FalseLabel          string `json:"falseLabel,omitempty"`
	// switch
	Cases []SwitchCase `json:"cases,omitempty"`
	// http_request
	URL       string            `json:"url,omitempty"`
	Method    string            `json:"method,omitempty"`
	Headers   map[string]string `json:"headers,omitempty"`
	Body      string            `json:"body,omitempty"`
	AuthType  string            `json:"authType,omitempty"`
	AuthValue string            `json:"authValue,omitempty"`
	// code_transform
	Code     string `json:"code,omitempty"`
	Language string `json:"language,omitempty"`
	// wait
	DelayValue int    `json:"delayValue,omitempty"`
	DelayUnit  string `json:"delayUnit,omitempty"`
	// loop
	IterateOver string `json:"iterateOver,omitempty"`
	LoopVar     string `json:"loopVar,omitempty"`
	// set_variable
	Variables []WorkflowVar `json:"variables,omitempty"`
	// workspace_op
	Operation     string `json:"operation,omitempty"`
	FilePath      string `json:"filePath,omitempty"`
	Content       string `json:"content,omitempty"`
	ContentSource string `json:"contentSource,omitempty"`
	SearchQuery   string `json:"searchQuery,omitempty"`
	// shell_command
	ShellCommand     string `json:"shellCommand,omitempty"`
	ShellCwd         string `json:"shellCwd,omitempty"`
	ShellAutoApprove bool   `json:"shellAutoApprove,omitempty"`
	// email_send (the email body reuses Body / "body" above)
	To        string `json:"to,omitempty"`
	Cc        string `json:"cc,omitempty"`
	Bcc       string `json:"bcc,omitempty"`
	Subject   string `json:"subject,omitempty"`
	EmailHTML bool   `json:"emailHtml,omitempty"`
	FromName  string `json:"fromName,omitempty"`
	// trigger_email (inbound IMAP poll)
	EmailFolder string `json:"emailFolder,omitempty"` // default INBOX
	// send_channel (the message text reuses Body / "body" above)
	ChannelID     string `json:"channelId,omitempty"`     // configured channel to send through
	ChannelTarget string `json:"channelTarget,omitempty"` // chat/channel id, or phone for whatsapp
	// llm_prompt (the user prompt reuses TaskPrompt / "taskPrompt" above)
	SystemPrompt string `json:"systemPrompt,omitempty"`
	LLMJSON      bool   `json:"llmJson,omitempty"` // parse the model's reply as JSON
	// web (operation reuses Operation: "search" | "fetch"; url reuses URL; query reuses SearchQuery)
	MaxResults int `json:"maxResults,omitempty"`
	// knowledge_base (operation reuses Operation: "search" | "ingest";
	//   search: SearchQuery + TopK; ingest file: FilePath; ingest text: Content/ContentSource + KBSourceName)
	TopK         int    `json:"topK,omitempty"`
	KBSourceName string `json:"kbSourceName,omitempty"`
	// sql_query
	SQLDriver string `json:"sqlDriver,omitempty"` // currently: postgres
	SQLDSN    string `json:"sqlDsn,omitempty"`    // connection string / DSN
	SQLQuery  string `json:"sqlQuery,omitempty"`
	// filter (keep the array items for which FilterExpression is truthy)
	FilterExpression string `json:"filterExpression,omitempty"`
	// template (render a {{ }} template; optionally wrap the result under TemplateField)
	Template      string `json:"template,omitempty"`
	TemplateField string `json:"templateField,omitempty"`
	// approval (human-in-the-loop gate)
	ApprovalTitle     string `json:"approvalTitle,omitempty"`
	ApprovalMessage   string `json:"approvalMessage,omitempty"`
	ApprovalTimeout   int    `json:"approvalTimeout,omitempty"`   // minutes; 0 = default 60
	ApprovalOnTimeout string `json:"approvalOnTimeout,omitempty"` // "approve" | "reject" (default reject)
	// sub_workflow
	SubWorkflowID string `json:"subWorkflowId,omitempty"`
	// error handling (all executable nodes)
	OnError      string `json:"onError,omitempty"` // stop|continue|retry (default stop)
	RetryCount   int    `json:"retryCount,omitempty"`
	RetryDelayMs int    `json:"retryDelayMs,omitempty"`
	// output binding
	OutputVar string `json:"outputVar,omitempty"`
}

WorkflowNodeConfig is a wide optional-field struct; omitempty keeps the JSON round-tripping 1:1 with the TS optional-field object.

type WorkflowRun

type WorkflowRun struct {
	ID             string         `json:"id"`
	WorkflowID     string         `json:"workflowId"`
	Status         RunStatus      `json:"status"`
	TriggerType    string         `json:"triggerType"`
	TriggerPayload map[string]any `json:"triggerPayload,omitempty"`
	Error          string         `json:"error,omitempty"`
	StartedAt      time.Time      `json:"startedAt"`
	CompletedAt    *time.Time     `json:"completedAt,omitempty"`
	NodeStates     []NodeRunState `json:"nodeStates,omitempty"`
}

type WorkflowVar

type WorkflowVar struct {
	Name        string `json:"name"`
	Value       string `json:"value"`
	Description string `json:"description,omitempty"`
}

WorkflowVar matches the desktop { name, value, description? } shape.

Jump to

Keyboard shortcuts

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