This guide explains how to add custom behaviors to workflow components by following the existing architecture patterns. The component system uses registries to manage different types of customizations.
The component system is organized in the following directory structure:
web_src/src/pages/workflowv2/mappers/
├── index.ts # Main registry file - all registrations happen here
├── types.ts # TypeScript interfaces for all customization types
├── stateRegistry.ts # Default state registry and fallback state logic
├── default.ts # Default trigger renderer implementation
├── approval.ts # Approval component with custom states, data builder
├── wait.tsx # Wait component with custom field renderer
├── schedule.ts # Schedule trigger with custom field renderer
├── github.ts # GitHub-specific trigger renderer
├── semaphore.ts # Semaphore component mapper
├── http.ts # HTTP component mapper
├── if.ts # If/conditional component mapper
├── filter.ts # Filter component mapper
├── timegate.ts # Time gate component mapper
├── noop.ts # No-operation component mapper
└── semaphore/ # Directory for semaphore app-specific mappers
└── index.ts # Semaphore app component and trigger registries
index.ts- This is where ALL component customizations are registered. Every new customization must be added here.types.ts- Contains TypeScript interfaces that define the contracts for all customization types.stateRegistry.ts- Provides the default state logic that most components inherit from.- Component files (e.g.,
approval.ts,wait.tsx) - Individual component implementations with their specific customizations.
The main registry file web_src/src/pages/workflowv2/mappers/index.ts manages 6 types of customizations:
Location: web_src/src/pages/workflowv2/mappers/index.ts lines 35-44
Purpose: Maps component rendering logic and properties.
Current registrations: noop, if, http, semaphore, timeGate, filter, wait, approval
const componentBaseMappers: Record<string, ComponentBaseMapper> = {
noop: noopMapper, // from ./noop.ts
if: ifMapper, // from ./if.ts
http: httpMapper, // from ./http.ts
semaphore: oldSemaphoreMapper, // from ./semaphore.ts
timeGate: timeGateMapper, // from ./timegate.ts
filter: filterMapper, // from ./filter.ts
wait: waitMapper, // from ./wait.tsx
approval: approvalMapper, // from ./approval.ts
};Location: web_src/src/pages/workflowv2/mappers/index.ts lines 30-33
Purpose: Handles how triggers are displayed and behave.
Current registrations: github, schedule
const triggerRenderers: Record<string, TriggerRenderer> = {
github: githubTriggerRenderer, // from ./github.ts
schedule: scheduleTriggerRenderer, // from ./schedule.ts
};Location: web_src/src/pages/workflowv2/mappers/index.ts lines 60-62
Purpose: Custom state logic and visual styling for different component states.
Current registrations: approval
const eventStateRegistries: Record<string, EventStateRegistry> = {
approval: APPROVAL_STATE_REGISTRY, // from ./approval.ts
};Location: web_src/src/pages/workflowv2/mappers/index.ts lines 64-67
Purpose: Renders additional UI elements in component settings.
Current registrations: schedule, wait
const customFieldRenderers: Record<string, CustomFieldRenderer> = {
schedule: scheduleCustomFieldRenderer, // from ./schedule.ts
wait: waitCustomFieldRenderer, // from ./wait.tsx
};Location: web_src/src/pages/workflowv2/mappers/index.ts lines 46-54
Purpose: For components that belong to specific applications (like semaphore., github.)
Current app registrations: semaphore, github
const appMappers: Record<string, Record<string, ComponentBaseMapper>> = {
semaphore: semaphoreComponentMappers, // from ./semaphore/index.ts
github: githubComponentMappers, // from ./github/index.ts
};
const appTriggerRenderers: Record<string, Record<string, TriggerRenderer>> = {
semaphore: semaphoreTriggerRenderers, // from ./semaphore/index.ts
github: githubTriggerRenderers, // from ./github/index.ts
};Primary file: web_src/src/pages/workflowv2/mappers/approval.ts
State map definition: Lines 37-69
State function definition: Lines 74-102
State registry creation: Lines 107-110
Registration location: web_src/src/pages/workflowv2/mappers/index.ts lines 60-62
The approval component demonstrates custom state logic:
export const APPROVAL_STATE_MAP: EventStateMap = {
...DEFAULT_EVENT_STATE_MAP, // Inherit defaults
waiting: {
icon: "clock",
textColor: "text-gray-800",
backgroundColor: "bg-orange-100",
badgeColor: "bg-yellow-600",
},
approved: {
icon: "circle-check",
textColor: "text-gray-800",
backgroundColor: "bg-green-100",
badgeColor: "bg-emerald-500",
},
rejected: {
icon: "circle-x",
textColor: "text-gray-800",
backgroundColor: "bg-red-100",
badgeColor: "bg-red-400",
},
};export const approvalStateFunction: StateFunction = (execution: WorkflowsWorkflowNodeExecution): EventState => {
// Error state - component could not evaluate
if (execution.state === "STATE_FINISHED" && execution.result === "RESULT_FAILED") {
return "error";
}
// Waiting state - actors haven't responded
if (execution.state === "STATE_PENDING" || execution.state === "STATE_STARTED") {
return "waiting";
}
// Check execution metadata for approval decision
if (execution.state === "STATE_FINISHED" && execution.result === "RESULT_PASSED") {
const metadata = execution.metadata as Record<string, any> | undefined;
if (metadata?.result === "approved") return "approved";
if (metadata?.result === "rejected") return "rejected";
return "approved"; // Default to success
}
return "error"; // Fallback
};export const APPROVAL_STATE_REGISTRY: EventStateRegistry = {
stateMap: APPROVAL_STATE_MAP,
getState: approvalStateFunction,
};In web_src/src/pages/workflowv2/mappers/index.ts:60-62:
const eventStateRegistries: Record<string, EventStateRegistry> = {
approval: APPROVAL_STATE_REGISTRY,
};Primary file: web_src/src/pages/workflowv2/mappers/wait.tsx
Custom field renderer definition: Lines 242-294
Registration location: web_src/src/pages/workflowv2/mappers/index.ts lines 64-67
Import statement location: web_src/src/pages/workflowv2/mappers/index.ts line 22
The wait component shows custom UI in the settings panel:
export const waitCustomFieldRenderer: CustomFieldRenderer = {
render: (node: NodeInfo) => {
const mode = node.configuration?.mode as string;
let content: string;
let title: string;
if (mode === "interval") {
title = "Fixed Time Interval";
content = `Component will wait for a fixed amount of time...
Example expressions:
{{ $.wait_time }}
{{ $.wait_time + 5 }}`;
} else if (mode === "countdown") {
title = "Countdown to Date/Time";
content = `Component will countdown until the provided date/time...
Example expressions:
{{ $.run_time }}
{{ date($.date_string) }}`;
} else {
title = "Wait Component";
content = "Configure the wait mode to see more details.";
}
return (
<div className="border-t-1 border-gray-200 pt-4">
<div className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-700">{title}:</span>
<div className="text-sm text-gray-800 mt-1 border-1 p-3 bg-gray-50 rounded-md font-mono whitespace-pre-line">
{content}
</div>
</div>
</div>
</div>
);
},
};In web_src/src/pages/workflowv2/mappers/index.ts:64-67:
const customFieldRenderers: Record<string, CustomFieldRenderer> = {
schedule: scheduleCustomFieldRenderer,
wait: waitCustomFieldRenderer,
};To create a new component with custom behaviors:
Location: web_src/src/pages/workflowv2/mappers/mycomponent.ts
Required imports: From ./types and any UI components you need
Follow naming convention: File name should match component type name
Create web_src/src/pages/workflowv2/mappers/mycomponent.ts:
import {
ComponentBaseMapper,
ComponentBaseContext,
ExecutionDetailsContext,
EventStateRegistry,
NodeInfo,
SubtitleContext,
CustomFieldRenderer,
} from "./types";
import { DEFAULT_EVENT_STATE_MAP } from "@/ui/componentBase";
// Custom state map (optional)
export const MY_COMPONENT_STATE_MAP = {
...DEFAULT_EVENT_STATE_MAP,
processing: {
icon: "loader",
textColor: "text-blue-800",
backgroundColor: "bg-blue-100",
badgeColor: "bg-blue-500",
},
};
// Custom state function (optional)
export const myComponentStateFunction = (execution) => {
if (execution.metadata?.status === "processing") return "processing";
// ... other logic
return defaultStateFunction(execution);
};
// State registry (optional)
export const MY_COMPONENT_STATE_REGISTRY: EventStateRegistry = {
stateMap: MY_COMPONENT_STATE_MAP,
getState: myComponentStateFunction,
};
// Base mapper (required)
export const myComponentMapper: ComponentBaseMapper = {
props(context: ComponentBaseContext) {
return {
iconSlug: context.componentDefinition.icon || "box",
iconColor: "text-blue-600",
headerColor: "bg-white",
title: context.node.name || "My Component",
// ... other properties
};
},
subtitle(context: SubtitleContext) {
return context.execution.metadata?.customMessage || "Processing...";
},
getExecutionDetails(context: ExecutionDetailsContext) {
return context.execution.metadata || {};
},
};
// Custom field renderer (optional)
export const myComponentCustomFieldRenderer: CustomFieldRenderer = {
render: (node: NodeInfo) => {
return (
<div className="p-4">
<p>Custom configuration UI for {node.name}</p>
<pre>{JSON.stringify(configuration, null, 2)}</pre>
</div>
);
},
};File to modify: web_src/src/pages/workflowv2/mappers/index.ts
Add import statements: Near the top of the file with other imports
Add to registries: In the appropriate registry objects (lines 35-67)
Follow existing patterns: Look at how other components are registered
In web_src/src/pages/workflowv2/mappers/index.ts, add imports and register:
import {
myComponentMapper,
MY_COMPONENT_STATE_REGISTRY,
myComponentCustomFieldRenderer
} from "./mycomponent";
// Add to registries
const componentBaseMappers: Record<string, ComponentBaseMapper> = {
// ... existing mappers
mycomponent: myComponentMapper,
};
const eventStateRegistries: Record<string, EventStateRegistry> = {
// ... existing registries
mycomponent: MY_COMPONENT_STATE_REGISTRY,
};
const customFieldRenderers: Record<string, CustomFieldRenderer> = {
// ... existing renderers
mycomponent: myComponentCustomFieldRenderer,
};- Follow naming conventions: Component file names should match the component type
- Extend defaults: Always extend
DEFAULT_EVENT_STATE_MAPrather than replacing it - Error handling: Include proper error states and fallbacks in state functions
- Type safety: Use proper TypeScript types from
types.ts - Performance: Cache expensive operations in additional data builders
- Consistency: Follow existing patterns for UI styling and interactions
The registry provides several helper functions in web_src/src/pages/workflowv2/mappers/index.ts:
Location of helper functions: Lines 73-147
Default fallbacks: Defined in web_src/src/pages/workflowv2/mappers/stateRegistry.ts and web_src/src/pages/workflowv2/mappers/default.ts
Available helper functions:
getTriggerRenderer(name): Lines 73-87 - Get trigger renderer with app supportgetComponentBaseMapper(name): Lines 93-107 - Get component mapper with app supportgetEventStateRegistry(name): Lines 121-123 - Get state registry with fallbackgetStateMap(name): Lines 129-131 - Get state mapgetState(name): Lines 137-139 - Get state functiongetCustomFieldRenderer(name): Lines 145-147 - Get custom field renderer
These functions handle the lookup logic and provide fallbacks to default implementations.
When you need to add new visual properties or behaviors to components:
File: web_src/src/ui/componentBase/index.tsx
Interface location: Lines 187-212
Add your new prop to the ComponentBaseProps interface:
export interface ComponentBaseProps extends ComponentActionsProps {
// ... existing props
myCustomProp?: string; // Add your new prop here
myCustomBehavior?: boolean; // Or multiple props as needed
}File: web_src/src/ui/componentBase/index.tsx
Component definition: Lines 214-401
Add prop to destructuring (around line 214) and use it in JSX:
export const ComponentBase: React.FC<ComponentBaseProps> = ({
// ... existing props
myCustomProp,
myCustomBehavior,
// ... rest of props
}) => {
// Use your prop in the component logic or JSX
return (
<div className={`${myCustomBehavior ? 'custom-class' : ''}`}>
{myCustomProp && <span>{myCustomProp}</span>}
{/* ... rest of component */}
</div>
);
};Files: Various mapper files (e.g., web_src/src/pages/workflowv2/mappers/approval.ts)
Update the props method in your component's mapper to include the new prop:
export const myComponentMapper: ComponentBaseMapper = {
props(context: ComponentBaseContext): ComponentBaseProps {
return {
// ... existing props
myCustomProp: "Custom value based on component logic",
myCustomBehavior: context.lastExecutions.length > 0,
// ... rest of props
};
},
};Let's say you want to add a statusBadge prop:
Location: web_src/src/ui/componentBase/index.tsx:187-212
export interface ComponentBaseProps extends ComponentActionsProps {
// ... existing props
statusBadge?: {
text: string;
color: string;
};
}Location: web_src/src/ui/componentBase/index.tsx:214-401
export const ComponentBase: React.FC<ComponentBaseProps> = ({
// ... existing props
statusBadge,
}) => {
return (
<div>
<ComponentHeader /* ... */ />
{statusBadge && (
<div className={`px-2 py-1 text-xs font-semibold rounded ${statusBadge.color}`}>
{statusBadge.text}
</div>
)}
{/* ... rest of component */}
</div>
);
};Location: web_src/src/pages/workflowv2/mappers/approval.ts:120-138
export const approvalMapper: ComponentBaseMapper = {
props(context: ComponentBaseContext): ComponentBaseProps {
const lastExecution = context.lastExecutions[0];
return {
// ... existing props
statusBadge: lastExecution?.state === "STATE_STARTED" ? {
text: "Awaiting Approval",
color: "bg-orange-100 text-yellow-800"
} : undefined,
};
},
};For designers who need to quickly locate files:
- Main registry:
web_src/src/pages/workflowv2/mappers/index.ts - Type definitions:
web_src/src/pages/workflowv2/mappers/types.ts - ComponentBase UI:
web_src/src/ui/componentBase/index.tsx - ComponentBaseProps interface:
web_src/src/ui/componentBase/index.tsx(lines 187-212) - Default state logic:
web_src/src/pages/workflowv2/mappers/stateRegistry.ts - Example custom states:
web_src/src/pages/workflowv2/mappers/approval.ts(lines 37-110) - Example custom field:
web_src/src/pages/workflowv2/mappers/wait.tsx(lines 242-294) - Example data builder:
web_src/src/pages/workflowv2/mappers/approval.ts(lines 257-398) - App-specific example:
web_src/src/pages/workflowv2/mappers/semaphore/index.ts