diff --git a/apps/api/src/services/WorkflowService.ts b/apps/api/src/services/WorkflowService.ts
index 9d64c2a..5253312 100644
--- a/apps/api/src/services/WorkflowService.ts
+++ b/apps/api/src/services/WorkflowService.ts
@@ -194,25 +194,57 @@ export class WorkflowService {
}
}
- const updateData: Prisma.WorkflowUpdateInput = {};
+ // Use transaction to update workflow and TRIGGER step atomically
+ const updated = await prisma.$transaction(async tx => {
+ const updateData: Prisma.WorkflowUpdateInput = {};
- if (data.name !== undefined) updateData.name = data.name;
- if (data.description !== undefined) updateData.description = data.description;
- if (data.triggerType !== undefined) updateData.triggerType = data.triggerType;
- if (data.triggerConfig !== undefined) {
- updateData.triggerConfig = data.triggerConfig === null ? Prisma.JsonNull : data.triggerConfig;
- }
- if (data.enabled !== undefined) updateData.enabled = data.enabled;
- if (data.allowReentry !== undefined) updateData.allowReentry = data.allowReentry;
+ if (data.name !== undefined) updateData.name = data.name;
+ if (data.description !== undefined) updateData.description = data.description;
+ if (data.triggerType !== undefined) updateData.triggerType = data.triggerType;
+ if (data.triggerConfig !== undefined) {
+ updateData.triggerConfig = data.triggerConfig === null ? Prisma.JsonNull : data.triggerConfig;
+ }
+ if (data.enabled !== undefined) updateData.enabled = data.enabled;
+ if (data.allowReentry !== undefined) updateData.allowReentry = data.allowReentry;
- const updated = await prisma.workflow.update({
- where: {id: workflowId},
- data: updateData,
- include: {
- project: {
- select: {name: true},
+ const updatedWorkflow = await tx.workflow.update({
+ where: {id: workflowId},
+ data: updateData,
+ include: {
+ project: {
+ select: {name: true},
+ },
},
- },
+ });
+
+ // If triggerConfig changed and it's an EVENT trigger, update the TRIGGER step
+ if (data.triggerConfig !== undefined && updatedWorkflow.triggerType === 'EVENT') {
+ const newTriggerConfig = data.triggerConfig as {eventName?: string} | null;
+ const eventName = newTriggerConfig?.eventName;
+
+ if (eventName) {
+ // Find the TRIGGER step
+ const triggerStep = await tx.workflowStep.findFirst({
+ where: {
+ workflowId: workflowId,
+ type: 'TRIGGER',
+ },
+ });
+
+ if (triggerStep) {
+ // Update TRIGGER step config and name to match
+ await tx.workflowStep.update({
+ where: {id: triggerStep.id},
+ data: {
+ name: `Trigger: ${eventName}`,
+ config: {eventName},
+ },
+ });
+ }
+ }
+ }
+
+ return updatedWorkflow;
});
// Invalidate workflow cache if enabled status changed or workflow is enabled
@@ -220,6 +252,11 @@ export class WorkflowService {
await EventService.invalidateWorkflowCache(projectId);
}
+ // Also invalidate cache if triggerConfig changed on an enabled workflow
+ if (data.triggerConfig !== undefined && updated.enabled) {
+ await EventService.invalidateWorkflowCache(projectId);
+ }
+
// Send notification if enabled status changed
if (data.enabled !== undefined && data.enabled !== workflow.enabled) {
if (data.enabled) {
diff --git a/apps/web/src/components/WorkflowBuilder.tsx b/apps/web/src/components/WorkflowBuilder.tsx
index 832cea3..98704b5 100644
--- a/apps/web/src/components/WorkflowBuilder.tsx
+++ b/apps/web/src/components/WorkflowBuilder.tsx
@@ -226,6 +226,22 @@ function CustomNode({
onMouseLeave={() => setShowActions(false)}
>
{/* Action buttons - shown on hover */}
+ {showActions && data.type === 'TRIGGER' && (
+
@@ -375,11 +399,23 @@ export function WorkflowBuilder({workflowId, steps, onUpdate}: WorkflowBuilderPr
const [stepToDelete, setStepToDelete] = useState(null);
// Define handlers before they are used in useMemo
- const handleEditStep = useCallback((stepId: string) => {
- // This will be handled by the parent component
- const event = new CustomEvent('workflow-edit-step', {detail: {stepId}});
- window.dispatchEvent(event);
- }, []);
+ const handleEditStep = useCallback(
+ (stepId: string) => {
+ // Check if this is a TRIGGER step
+ const step = steps.find(s => s.id === stepId);
+
+ if (step?.type === 'TRIGGER') {
+ // For TRIGGER steps, open workflow settings instead
+ const event = new CustomEvent('workflow-open-settings');
+ window.dispatchEvent(event);
+ } else {
+ // For other steps, open step editor
+ const event = new CustomEvent('workflow-edit-step', {detail: {stepId}});
+ window.dispatchEvent(event);
+ }
+ },
+ [steps],
+ );
const handleDeleteStepClick = useCallback((stepId: string) => {
setStepToDelete(stepId);
diff --git a/apps/web/src/pages/workflows/[id].tsx b/apps/web/src/pages/workflows/[id].tsx
index 0ccdef5..df336c5 100644
--- a/apps/web/src/pages/workflows/[id].tsx
+++ b/apps/web/src/pages/workflows/[id].tsx
@@ -318,7 +318,12 @@ export default function WorkflowEditorPage() {
}
};
- const handleUpdateSettings = async (data: {name: string; description?: string}) => {
+ const handleUpdateSettings = async (data: {
+ name: string;
+ description?: string;
+ allowReentry?: boolean;
+ triggerConfig?: {eventName: string};
+ }) => {
try {
await network.fetch('PATCH', `/workflows/${id}`, data);
toast.success('Workflow updated successfully');
@@ -352,9 +357,15 @@ export default function WorkflowEditorPage() {
}
};
+ const handleOpenSettingsEvent = () => {
+ setShowSettingsDialog(true);
+ };
+
window.addEventListener('workflow-edit-step', handleEditStepEvent);
+ window.addEventListener('workflow-open-settings', handleOpenSettingsEvent);
return () => {
window.removeEventListener('workflow-edit-step', handleEditStepEvent);
+ window.removeEventListener('workflow-open-settings', handleOpenSettingsEvent);
};
}, [workflow]);
@@ -741,21 +752,49 @@ interface SettingsDialogProps {
workflow: Workflow;
open: boolean;
onOpenChange: (open: boolean) => void;
- onSave: (data: {name: string; description?: string; allowReentry?: boolean}) => Promise;
+ onSave: (data: {
+ name: string;
+ description?: string;
+ allowReentry?: boolean;
+ triggerConfig?: {eventName: string};
+ }) => Promise;
}
function SettingsDialog({workflow, open, onOpenChange, onSave}: SettingsDialogProps) {
+ const triggerConfig = workflow.triggerConfig as {eventName?: string} | null;
const [name, setName] = useState(workflow.name);
const [description, setDescription] = useState(workflow.description ?? '');
const [allowReentry, setAllowReentry] = useState(workflow.allowReentry ?? false);
+ const [eventName, setEventName] = useState(triggerConfig?.eventName ?? '');
const [isSubmitting, setIsSubmitting] = useState(false);
+ // Sync state when workflow changes or dialog opens
+ useEffect(() => {
+ if (open) {
+ setName(workflow.name);
+ setDescription(workflow.description ?? '');
+ setAllowReentry(workflow.allowReentry ?? false);
+ const config = workflow.triggerConfig as {eventName?: string} | null;
+ setEventName(config?.eventName ?? '');
+ }
+ }, [open, workflow]);
+
+ // Fetch available event names
+ const {data: eventNamesData} = useSWR<{eventNames: string[]}>(open ? '/events/names' : null, {
+ revalidateOnFocus: false,
+ });
+
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
- await onSave({name, description: description || undefined, allowReentry});
+ await onSave({
+ name,
+ description: description || undefined,
+ allowReentry,
+ triggerConfig: eventName.trim() ? {eventName: eventName.trim()} : undefined,
+ });
} finally {
setIsSubmitting(false);
}
@@ -784,6 +823,36 @@ function SettingsDialog({workflow, open, onOpenChange, onSave}: SettingsDialogPr
/>
+
+
+ {eventNamesData?.eventNames && eventNamesData.eventNames.length > 0 ? (
+
+ ) : (
+
setEventName(e.target.value)}
+ placeholder="e.g., contact.created, email.opened"
+ required
+ />
+ )}
+
+ The event that triggers this workflow to start for a contact
+
+
+
diff --git a/packages/shared/src/schemas/index.ts b/packages/shared/src/schemas/index.ts
index 2aa61f1..b231984 100644
--- a/packages/shared/src/schemas/index.ts
+++ b/packages/shared/src/schemas/index.ts
@@ -180,6 +180,7 @@ export const WorkflowSchemas = {
triggerType: z.nativeEnum(WorkflowTriggerType).optional(),
triggerConfig: jsonSchema.optional(),
enabled: z.boolean().optional(),
+ allowReentry: z.boolean().optional(),
}),
addStep: z.object({
type: z.nativeEnum(WorkflowStepType),