Trigger
-
- {(['manual', 'webhook'] as TriggerMode[]).map(mode => {
+
+ {([
+ { mode: 'manual' as TriggerMode, label: 'Manual' },
+ { mode: 'webhook' as TriggerMode, label: 'Webhook' },
+ { mode: 'schedule' as TriggerMode, label: 'Schedule' },
+ { mode: 'event' as TriggerMode, label: 'Event' },
+ { mode: 'chat' as TriggerMode, label: 'Chat' },
+ ]).map(({ mode, label }) => {
const active = trigger.type === mode
return (
)
})}
@@ -105,7 +110,7 @@ export function WorkflowInspector({
- {/* Webhook config (conditional) */}
+ {/* Webhook config */}
{trigger.type === 'webhook' && (
<>
@@ -114,7 +119,7 @@ export function WorkflowInspector({
className="nbl-input"
value={trigger.config?.slug ?? ''}
placeholder="auto-generated if empty"
- onChange={e => onUpdateTrigger({ config: { slug: e.target.value } })}
+ onChange={e => onUpdateTrigger({ config: { ...trigger.config, slug: e.target.value } })}
/>
@@ -123,12 +128,75 @@ export function WorkflowInspector({
className="nbl-input"
value={trigger.config?.agent_id ?? ''}
placeholder="running agent id"
- onChange={e => onUpdateTrigger({ config: { agent_id: e.target.value } })}
+ onChange={e => onUpdateTrigger({ config: { ...trigger.config, agent_id: e.target.value } })}
/>
>
)}
+ {/* Schedule config */}
+ {trigger.type === 'schedule' && (
+ <>
+
+
+ onUpdateTrigger({ config: { ...trigger.config, cron: e.target.value } })}
+ />
+
+
+
+ onUpdateTrigger({ config: { ...trigger.config, timezone: e.target.value } })}
+ />
+
+
+
+ onUpdateTrigger({ config: { ...trigger.config, agent_id: e.target.value } })}
+ />
+
+ >
+ )}
+
+ {/* Event config */}
+ {trigger.type === 'event' && (
+
+
+
+ )}
+
+ {/* Chat trigger config */}
+ {trigger.type === 'chat' && (
+
+
+ onUpdateTrigger({ config: { ...trigger.config, command: e.target.value } })}
+ />
+
+ )}
+
{/* Template reference */}
diff --git a/src/pages/workflows/components/node-registry.ts b/src/pages/workflows/components/node-registry.ts
index b2029a2..4837689 100644
--- a/src/pages/workflows/components/node-registry.ts
+++ b/src/pages/workflows/components/node-registry.ts
@@ -42,12 +42,23 @@ const NODE_TYPE_ENTRIES: NodeTypeEntry[] = [
{ type: 'text_input', family: 'input', label: 'Text Input', description: 'Collect text for downstream reasoning or tools', icon: NODE_ICON.text_input, color: NODE_COLOR.text_input, idPrefix: 'text' },
{ type: 'file_input', family: 'input', label: 'File Input', description: 'Provide a file path or artifact reference', icon: NODE_ICON.file_input, color: NODE_COLOR.file_input, idPrefix: 'file' },
{ type: 'image_input', family: 'input', label: 'Image Input', description: 'Provide an image path for downstream nodes', icon: NODE_ICON.image_input, color: NODE_COLOR.image_input, idPrefix: 'image' },
- // Engine
+ // Engine (AI)
{ type: 'llm_reason', family: 'engine', label: 'LLM Reasoning', description: 'Send a prompt to an LLM for reasoning', icon: NODE_ICON.llm_reason, color: NODE_COLOR.llm_reason, idPrefix: 'reason' },
{ type: 'tool_call', family: 'engine', label: 'Tool Call', description: 'Execute a registered tool with structured input', icon: NODE_ICON.tool_call, color: NODE_COLOR.tool_call, idPrefix: 'tool' },
- { type: 'condition', family: 'engine', label: 'Condition', description: 'Branch execution based on a condition', icon: NODE_ICON.condition, color: NODE_COLOR.condition, idPrefix: 'condition' },
+ // Logic / Control Flow
+ { type: 'condition', family: 'logic', label: 'Condition', description: 'Binary branch (if/else) based on expression or LLM judge', icon: NODE_ICON.condition, color: NODE_COLOR.condition, idPrefix: 'cond' },
+ { type: 'switch', family: 'logic', label: 'Switch', description: 'Multi-way branch with named cases', icon: NODE_ICON.switch, color: NODE_COLOR.switch, idPrefix: 'switch' },
+ { type: 'loop', family: 'logic', label: 'Loop', description: 'Iterate over a list with configurable concurrency', icon: NODE_ICON.loop, color: NODE_COLOR.loop, idPrefix: 'loop' },
+ { type: 'merge', family: 'logic', label: 'Merge', description: 'Wait for multiple branches to complete', icon: NODE_ICON.merge, color: NODE_COLOR.merge, idPrefix: 'merge' },
+ { type: 'delay', family: 'logic', label: 'Delay', description: 'Wait for a duration or condition before continuing', icon: NODE_ICON.delay, color: NODE_COLOR.delay, idPrefix: 'delay' },
+ // Integration / Tool
+ { type: 'http_request', family: 'tool', label: 'HTTP Request', description: 'Make an HTTP request to an external API', icon: NODE_ICON.http_request, color: NODE_COLOR.http_request, idPrefix: 'http' },
+ { type: 'code', family: 'tool', label: 'Code', description: 'Execute sandboxed JavaScript or Python code', icon: NODE_ICON.code, color: NODE_COLOR.code, idPrefix: 'code' },
+ // Gate
+ { type: 'human_approval', family: 'gate', label: 'Approval Gate', description: 'Pause for human review and approval', icon: NODE_ICON.human_approval, color: NODE_COLOR.human_approval, idPrefix: 'gate' },
// Output
{ type: 'output', family: 'output', label: 'Output', description: 'Create a first-class workflow output', icon: NODE_ICON.output, color: NODE_COLOR.output, idPrefix: 'output' },
+ { type: 'notification', family: 'output', label: 'Notification', description: 'Send notification via email, Slack, or in-app', icon: NODE_ICON.notification, color: NODE_COLOR.notification, idPrefix: 'notify' },
]
const NODE_TYPE_MAP = new Map(NODE_TYPE_ENTRIES.map(e => [e.type, e]))
@@ -106,7 +117,10 @@ export function getPaletteGroups(filter?: string): PaletteGroup[] {
const groups: PaletteGroup[] = []
const familyOrder: { family: WfNodeFamily; label: string }[] = [
{ family: 'input', label: 'Input' },
- { family: 'engine', label: 'Engine' },
+ { family: 'engine', label: 'AI / Engine' },
+ { family: 'logic', label: 'Control Flow' },
+ { family: 'tool', label: 'Integration' },
+ { family: 'gate', label: 'Governance' },
{ family: 'output', label: 'Output' },
]
for (const { family, label } of familyOrder) {
@@ -122,7 +136,19 @@ export function getPaletteGroups(filter?: string): PaletteGroup[] {
export const INPUT_NODE_TYPES: WfNodeType[] = ['text_input', 'file_input', 'image_input']
/** Node types within the Engine family (for inspector type morph) */
-export const ENGINE_NODE_TYPES: WfNodeType[] = ['llm_reason', 'tool_call', 'condition']
+export const ENGINE_NODE_TYPES: WfNodeType[] = ['llm_reason', 'tool_call']
+
+/** Node types within the Logic family */
+export const LOGIC_NODE_TYPES: WfNodeType[] = ['condition', 'switch', 'loop', 'merge', 'delay']
+
+/** Node types within the Tool/Integration family */
+export const TOOL_NODE_TYPES: WfNodeType[] = ['http_request', 'code']
+
+/** Node types within the Gate family */
+export const GATE_NODE_TYPES: WfNodeType[] = ['human_approval']
+
+/** Node types within the Output family */
+export const OUTPUT_NODE_TYPES: WfNodeType[] = ['output', 'notification']
/** Output format options */
export const OUTPUT_FORMAT_OPTIONS: { value: WfOutputFormat; label: string }[] = [
@@ -201,7 +227,115 @@ export function buildDefaultNode(def: WfDefinition, type: WfNodeType, label: str
}
}
if (type === 'condition') {
- return { id, type, family, label, description: '', runtime_params: [] }
+ return {
+ id, type, family, label,
+ description: 'Branch based on expression or LLM judge',
+ config: { condition_mode: 'expression', expression: '' },
+ runtime_params: [],
+ }
+ }
+ if (type === 'switch') {
+ return {
+ id, type, family, label,
+ description: 'Multi-way branch with named cases',
+ config: {
+ condition_mode: 'expression',
+ expression: '',
+ cases: [
+ { label: 'Case A', value: 'a', handle: 'case_1' },
+ { label: 'Case B', value: 'b', handle: 'case_2' },
+ ],
+ default_handle: 'default',
+ },
+ runtime_params: [],
+ }
+ }
+ if (type === 'loop') {
+ return {
+ id, type, family, label,
+ description: 'Iterate over a collection',
+ config: {
+ source_expression: '',
+ item_variable: 'item',
+ index_variable: 'index',
+ concurrency: 1,
+ max_iterations: 1000,
+ collect_results: true,
+ },
+ runtime_params: [],
+ }
+ }
+ if (type === 'merge') {
+ return {
+ id, type, family, label,
+ description: 'Wait for branches to complete',
+ config: {
+ merge_mode: 'all',
+ merge_timeout_seconds: 300,
+ merge_on_timeout: 'fail',
+ },
+ runtime_params: [],
+ }
+ }
+ if (type === 'delay') {
+ return {
+ id, type, family, label,
+ description: 'Wait before continuing',
+ config: { delay_seconds: 5 },
+ runtime_params: [],
+ }
+ }
+ if (type === 'http_request') {
+ return {
+ id, type, family, label,
+ description: 'Make an HTTP request',
+ config: {
+ http_method: 'GET',
+ http_url: '',
+ http_body_content_type: 'json',
+ http_auth_type: 'none',
+ http_timeout_ms: 30000,
+ },
+ runtime_params: [],
+ }
+ }
+ if (type === 'code') {
+ return {
+ id, type, family, label,
+ description: 'Execute sandboxed code',
+ config: {
+ code_language: 'python',
+ code_source: '# Write your code here\nresult = {"output": "hello"}',
+ code_timeout_seconds: 60,
+ code_network_allowed: false,
+ },
+ runtime_params: [],
+ }
+ }
+ if (type === 'human_approval') {
+ return {
+ id, type, family, label,
+ description: 'Pause for human review',
+ config: {
+ approval_title: 'Review required',
+ approval_timeout_seconds: 86400,
+ approval_timeout_action: 'fail',
+ approval_allow_modification: false,
+ approval_notify_channels: [],
+ },
+ runtime_params: [],
+ }
+ }
+ if (type === 'notification') {
+ return {
+ id, type, family, label,
+ description: 'Send a notification',
+ config: {
+ notify_channels: [{ type: 'agos_notification', target: '' }],
+ notify_template: '',
+ },
+ runtime_params: [],
+ }
}
if (type === 'output') {
return {
diff --git a/src/pages/workflows/components/workflow-node-constants.ts b/src/pages/workflows/components/workflow-node-constants.ts
index c9cbb06..2e6b5a9 100644
--- a/src/pages/workflows/components/workflow-node-constants.ts
+++ b/src/pages/workflows/components/workflow-node-constants.ts
@@ -1,5 +1,6 @@
import {
GitBranch, Play, CheckCircle2, Cpu, Zap, Type, FileText, Image as ImageIcon, ArrowDownToLine,
+ ArrowLeftRight, Repeat, Merge, Timer, Globe, Code2, ShieldCheck, Bell,
} from 'lucide-react'
import type { ElementType } from 'react'
import type { WfNodeType, WfNode, WfNodeFamily } from '../types'
@@ -12,6 +13,9 @@ export const NODE_FAMILY_COLOR: Record = {
system: 'var(--nbl-text-muted)',
input: 'var(--nbl-green)',
engine: 'var(--nbl-agos)',
+ logic: 'var(--nbl-amber)',
+ tool: 'var(--nbl-cyan)',
+ gate: 'var(--nbl-pink, #f472b6)',
output: 'var(--nbl-cyan)',
}
@@ -24,7 +28,15 @@ export const NODE_COLOR: Record = {
llm_reason: 'var(--nbl-agos)',
tool_call: 'var(--nbl-cyan)',
condition: 'var(--nbl-amber)',
+ switch: 'var(--nbl-amber)',
+ loop: 'var(--nbl-amber)',
+ merge: 'var(--nbl-amber)',
+ delay: 'var(--nbl-text-muted)',
+ http_request: 'var(--nbl-cyan)',
+ code: 'var(--nbl-agos)',
+ human_approval: 'var(--nbl-pink, #f472b6)',
output: 'var(--nbl-pink, #f472b6)',
+ notification: 'var(--nbl-pink, #f472b6)',
}
export const NODE_ICON: Record = {
@@ -36,7 +48,15 @@ export const NODE_ICON: Record = {
llm_reason: Cpu,
tool_call: Zap,
condition: GitBranch,
+ switch: ArrowLeftRight,
+ loop: Repeat,
+ merge: Merge,
+ delay: Timer,
+ http_request: Globe,
+ code: Code2,
+ human_approval: ShieldCheck,
output: ArrowDownToLine,
+ notification: Bell,
}
export const NODE_TYPE_LABEL: Record = {
@@ -48,13 +68,24 @@ export const NODE_TYPE_LABEL: Record = {
llm_reason: 'LLM Reasoning',
tool_call: 'Tool Call',
condition: 'Condition',
+ switch: 'Switch',
+ loop: 'Loop',
+ merge: 'Merge',
+ delay: 'Delay',
+ http_request: 'HTTP Request',
+ code: 'Code',
+ human_approval: 'Approval Gate',
output: 'Output',
+ notification: 'Notification',
}
-export const NODE_SHAPE_BY_FAMILY: Record = {
+export const NODE_SHAPE_BY_FAMILY: Record = {
system: 'pill',
input: 'pill',
engine: 'rect',
+ logic: 'diamond',
+ tool: 'rect',
+ gate: 'hex',
output: 'hex',
}
diff --git a/src/pages/workflows/types.ts b/src/pages/workflows/types.ts
index 81d899b..81b0609 100644
--- a/src/pages/workflows/types.ts
+++ b/src/pages/workflows/types.ts
@@ -1,6 +1,20 @@
-export type WfNodeType = 'llm_reason' | 'tool_call' | 'condition' | 'text_input' | 'file_input' | 'image_input' | 'output' | 'start' | 'end'
+export type WfNodeType =
+ // System
+ | 'start' | 'end'
+ // Input
+ | 'text_input' | 'file_input' | 'image_input'
+ // Engine (AI)
+ | 'llm_reason' | 'tool_call'
+ // Logic / Control Flow
+ | 'condition' | 'switch' | 'loop' | 'merge' | 'delay'
+ // Integration
+ | 'http_request' | 'code'
+ // Gate
+ | 'human_approval'
+ // Output
+ | 'output' | 'notification'
-export type WfNodeFamily = 'system' | 'input' | 'engine' | 'output'
+export type WfNodeFamily = 'system' | 'input' | 'engine' | 'logic' | 'tool' | 'gate' | 'output'
export type WfRuntimeParamType =
| 'text'
@@ -38,7 +52,7 @@ export type WfOutputRoute = {
export type WfOutputFormat = 'json' | 'md' | 'csv' | 'raw' | 'custom'
-export type WfTriggerType = 'manual' | 'webhook'
+export type WfTriggerType = 'manual' | 'webhook' | 'schedule' | 'event' | 'chat'
export type WfProviderType = 'agos-native' | 'n8n'
@@ -57,6 +71,13 @@ export type WfTriggerConfig = {
slug?: string
secret?: string
agent_id?: string
+ // Schedule
+ cron?: string
+ timezone?: string
+ // Event
+ event_types?: string[]
+ // Chat
+ command?: string
}
export type WfTrigger = {
@@ -65,18 +86,88 @@ export type WfTrigger = {
config: WfTriggerConfig
}
+export type WfSwitchCase = {
+ label: string
+ value: string
+ handle: string
+}
+
+export type WfNotifyChannel = {
+ type: 'email' | 'slack' | 'agos_notification'
+ target: string
+}
+
export type WfNodeConfig = {
+ // Input nodes
source_key?: string
input_kind?: 'text' | 'file' | 'image'
required?: boolean
default_value?: string
placeholder?: string
accept?: string
+ // Output nodes
output_format?: WfOutputFormat
value_template?: string
output_schema?: string
output_routes?: WfOutputRoute[]
+ // Tool call
tool_input?: string
+ // Condition / Switch
+ condition_mode?: 'expression' | 'llm_judge'
+ expression?: string
+ judge_prompt?: string
+ cases?: WfSwitchCase[]
+ default_handle?: string
+ // Loop
+ source_expression?: string
+ item_variable?: string
+ index_variable?: string
+ concurrency?: number
+ max_iterations?: number
+ collect_results?: boolean
+ // Merge
+ merge_mode?: 'all' | 'any' | 'first_n'
+ merge_n?: number
+ merge_timeout_seconds?: number
+ merge_on_timeout?: 'fail' | 'proceed_with_available'
+ // Delay
+ delay_seconds?: number
+ delay_until_expression?: string
+ // HTTP Request
+ http_method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'
+ http_url?: string
+ http_headers?: Record
+ http_body_template?: string
+ http_body_content_type?: 'json' | 'form' | 'text' | 'xml'
+ http_auth_type?: 'none' | 'bearer' | 'basic' | 'api_key' | 'credential_ref'
+ http_credential_id?: string
+ http_timeout_ms?: number
+ http_response_extract_path?: string
+ // Code
+ code_language?: 'javascript' | 'python'
+ code_source?: string
+ code_packages?: string[]
+ code_timeout_seconds?: number
+ code_network_allowed?: boolean
+ // Human approval
+ approval_title?: string
+ approval_description?: string
+ approval_timeout_seconds?: number
+ approval_timeout_action?: 'fail' | 'skip' | 'auto_approve'
+ approval_allow_modification?: boolean
+ approval_notify_channels?: WfNotifyChannel[]
+ // Notification
+ notify_channels?: WfNotifyChannel[]
+ notify_template?: string
+}
+
+export type WfNodeExecution = {
+ timeout_seconds?: number
+ retry_max?: number
+ retry_backoff_type?: 'fixed' | 'exponential'
+ retry_backoff_seconds?: number
+ on_error?: 'fail' | 'skip' | 'default_value'
+ default_value?: unknown
}
export type WfNode = {
@@ -87,15 +178,24 @@ export type WfNode = {
description?: string
tool?: string
prompt?: string
+ model?: string
+ temperature?: number
+ max_tokens?: number
+ structured_output_schema?: string
config?: WfNodeConfig
+ execution?: WfNodeExecution
runtime_params?: WfRuntimeParam[]
+ position?: { x: number; y: number }
}
export type WfEdge = {
id: string
source: string
target: string
+ source_handle?: string
+ target_handle?: string
label?: string
+ condition?: string
}
export type WfDefinition = {
@@ -110,7 +210,10 @@ export type WfDefinition = {
export function getNodeFamily(type: WfNodeType): WfNodeFamily {
if (type === 'start' || type === 'end') return 'system'
if (type === 'text_input' || type === 'file_input' || type === 'image_input') return 'input'
- if (type === 'output') return 'output'
+ if (type === 'condition' || type === 'switch' || type === 'loop' || type === 'merge' || type === 'delay') return 'logic'
+ if (type === 'http_request' || type === 'code') return 'tool'
+ if (type === 'human_approval') return 'gate'
+ if (type === 'output' || type === 'notification') return 'output'
return 'engine'
}
@@ -244,15 +347,21 @@ function parseNodeConfig(
return undefined
}
+/** All known node types for direct metadata matching */
+const KNOWN_NODE_TYPES = new Set([
+ 'start', 'end', 'text_input', 'file_input', 'image_input',
+ 'llm_reason', 'tool_call', 'condition', 'switch', 'loop', 'merge', 'delay',
+ 'http_request', 'code', 'human_approval', 'output', 'notification',
+])
+
function inferNodeType(
tool: string,
metadataType: string,
input: Record | undefined,
): WfNodeType {
- if (metadataType === 'condition') return 'condition'
- if (metadataType === 'text_input' || metadataType === 'file_input' || metadataType === 'image_input' || metadataType === 'output') {
- return metadataType
- }
+ // Direct match from metadata type
+ if (KNOWN_NODE_TYPES.has(metadataType)) return metadataType as WfNodeType
+
if (tool === 'workflow_input') {
const inputKind = String(input?.input_kind ?? '').trim()
if (inputKind === 'file') return 'file_input'
@@ -297,10 +406,13 @@ function nodeSpecToWfNode(spec: Record): WfNode {
}
}
+const VALID_TRIGGER_TYPES = new Set(['manual', 'webhook', 'schedule', 'event', 'chat'])
+
function parseTrigger(raw: Record): WfTrigger {
const trigger = raw.trigger as Record | undefined
const config = trigger?.config as Record | undefined
- const type = trigger?.type === 'webhook' ? 'webhook' : 'manual'
+ const rawType = String(trigger?.type ?? 'manual')
+ const type: WfTriggerType = VALID_TRIGGER_TYPES.has(rawType) ? rawType as WfTriggerType : 'manual'
return {
type,
enabled: trigger?.enabled !== false,
@@ -308,6 +420,10 @@ function parseTrigger(raw: Record): WfTrigger {
slug: typeof config?.slug === 'string' ? config.slug : undefined,
secret: typeof config?.secret === 'string' ? config.secret : undefined,
agent_id: typeof config?.agent_id === 'string' ? config.agent_id : undefined,
+ cron: typeof config?.cron === 'string' ? config.cron : undefined,
+ timezone: typeof config?.timezone === 'string' ? config.timezone : undefined,
+ event_types: Array.isArray(config?.event_types) ? config.event_types as string[] : undefined,
+ command: typeof config?.command === 'string' ? config.command : undefined,
},
}
}