Skip to content

WorkflowBuilder

A form-based workflow editor for creating and editing automation workflows. Providers can configure triggers, conditions, and actions without writing code.

Terminal window
npx thebookingkit add workflow-builder
import { WorkflowBuilder } from "./components/workflow-builder";
<WorkflowBuilder
triggers={availableTriggers}
actionTypes={availableActionTypes}
onSave={handleSaveWorkflow}
defaultValues={existingWorkflow}
/>
interface WorkflowBuilderProps {
/** Initial values for editing an existing workflow */
initialValues?: Partial<WorkflowFormValues>;
/** Available trigger options */
triggers?: TriggerOption[];
/** Available action type options */
actionTypes?: ActionTypeOption[];
/** Available condition field options */
conditionFields?: { value: string; label: string }[];
/** Called when the workflow is saved */
onSave: (values: WorkflowFormValues) => void;
/** Called when cancelled */
onCancel?: () => void;
/** Execution history logs */
executionLogs?: WorkflowLogDisplay[];
/** Additional CSS class name */
className?: string;
/** Inline styles */
style?: React.CSSProperties;
}
interface WorkflowFormValues {
name: string;
trigger: string;
conditions: BuilderCondition[];
actions: BuilderAction[];
isActive: boolean;
}
  1. Workflow Name — text input for naming the automation
  2. Trigger selection — dropdown of available triggers (booking_created, booking_confirmed, etc.)
  3. Conditions (optional) — add/remove conditions with field, operator, and value inputs
  4. Actions — add/remove actions with type-specific configuration:
    • Send email: to, subject, body with template variable insertion
    • Send SMS: to, message body
    • Fire webhook: webhook URL
    • Update status: new status selection
    • Create calendar event
  5. Active toggle — enable/disable the workflow
  6. Execution logs (optional) — recent execution history showing trigger, action type, status, and timestamps
import React, { useState, useCallback } from "react";
import { cn } from "../utils/cn.js";
/** Trigger option for the workflow builder */
export interface TriggerOption {
value: string;
label: string;
}
/** Action type option */
export interface ActionTypeOption {
value: string;
label: string;
}
/** A condition in the builder */
export interface BuilderCondition {
field: string;
operator: string;
value: string;
}
/** An action in the builder */
export interface BuilderAction {
type: string;
config: Record<string, string>;
}
/** The complete workflow form values */
export interface WorkflowFormValues {
name: string;
trigger: string;
conditions: BuilderCondition[];
actions: BuilderAction[];
isActive: boolean;
}
/** Execution log entry for display */
export interface WorkflowLogDisplay {
id: string;
actionType: string;
status: "success" | "error" | "skipped";
error?: string;
executedAt: Date;
bookingId?: string;
}
/** Props for the WorkflowBuilder component */
export interface WorkflowBuilderProps {
/** Initial values for editing an existing workflow */
initialValues?: Partial<WorkflowFormValues>;
/** Available trigger options */
triggers?: TriggerOption[];
/** Available action type options */
actionTypes?: ActionTypeOption[];
/** Available condition field options */
conditionFields?: { value: string; label: string }[];
/** Called when the workflow is saved */
onSave: (values: WorkflowFormValues) => void;
/** Called when cancelled */
onCancel?: () => void;
/** Execution history logs */
executionLogs?: WorkflowLogDisplay[];
/** Additional CSS class name */
className?: string;
/** Inline styles */
style?: React.CSSProperties;
}
const DEFAULT_TRIGGERS: TriggerOption[] = [
{ value: "booking_created", label: "Booking Created" },
{ value: "booking_confirmed", label: "Booking Confirmed" },
{ value: "booking_cancelled", label: "Booking Cancelled" },
{ value: "booking_rescheduled", label: "Booking Rescheduled" },
{ value: "before_event", label: "Before Event Start" },
{ value: "after_event", label: "After Event End" },
{ value: "payment_received", label: "Payment Received" },
{ value: "no_show_confirmed", label: "No-Show Confirmed" },
];
const DEFAULT_ACTION_TYPES: ActionTypeOption[] = [
{ value: "send_email", label: "Send Email" },
{ value: "send_sms", label: "Send SMS" },
{ value: "fire_webhook", label: "Fire Webhook" },
{ value: "update_status", label: "Update Status" },
{ value: "create_calendar_event", label: "Create Calendar Event" },
];
/**
* Visual workflow builder for creating trigger-condition-action automations.
*
* Guides the user through 4 steps:
* 1. Select trigger
* 2. Add conditions (optional)
* 3. Configure actions
* 4. Toggle active/inactive
*
* @example
* ```tsx
* <WorkflowBuilder
* onSave={(values) => createWorkflow(values)}
* executionLogs={logs}
* />
* ```
*/
export function WorkflowBuilder({
initialValues,
triggers = DEFAULT_TRIGGERS,
actionTypes = DEFAULT_ACTION_TYPES,
conditionFields = [
{ value: "eventTypeId", label: "Event Type" },
{ value: "status", label: "Booking Status" },
{ value: "customerEmail", label: "Customer Email" },
],
onSave,
onCancel,
executionLogs,
className,
style,
}: WorkflowBuilderProps) {
const [name, setName] = useState(initialValues?.name ?? "");
const [trigger, setTrigger] = useState(
initialValues?.trigger ?? triggers[0]?.value ?? "",
);
const [conditions, setConditions] = useState<BuilderCondition[]>(
initialValues?.conditions ?? [],
);
const [actions, setActions] = useState<BuilderAction[]>(
initialValues?.actions ?? [{ type: "send_email", config: {} }],
);
const [isActive, setIsActive] = useState(initialValues?.isActive ?? true);
const addCondition = useCallback(() => {
setConditions((prev) => [
...prev,
{ field: conditionFields[0]?.value ?? "", operator: "equals", value: "" },
]);
}, [conditionFields]);
const removeCondition = useCallback((index: number) => {
setConditions((prev) => prev.filter((_, i) => i !== index));
}, []);
const updateCondition = useCallback(
(index: number, updates: Partial<BuilderCondition>) => {
setConditions((prev) =>
prev.map((c, i) => (i === index ? { ...c, ...updates } : c)),
);
},
[],
);
const addAction = useCallback(() => {
setActions((prev) => [
...prev,
{ type: actionTypes[0]?.value ?? "send_email", config: {} },
]);
}, [actionTypes]);
const removeAction = useCallback((index: number) => {
setActions((prev) => prev.filter((_, i) => i !== index));
}, []);
const updateAction = useCallback(
(index: number, updates: Partial<BuilderAction>) => {
setActions((prev) =>
prev.map((a, i) => (i === index ? { ...a, ...updates } : a)),
);
},
[],
);
const updateActionConfig = useCallback(
(index: number, key: string, value: string) => {
setActions((prev) =>
prev.map((a, i) =>
i === index ? { ...a, config: { ...a.config, [key]: value } } : a,
),
);
},
[],
);
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
onSave({ name, trigger, conditions, actions, isActive });
},
[name, trigger, conditions, actions, isActive, onSave],
);
return (
<div className={cn("tbk-workflow-builder", className)} style={style}>
<form onSubmit={handleSubmit}>
{/* Workflow Name */}
<div className="tbk-field">
<label htmlFor="wf-name" className="tbk-label">
Workflow Name
</label>
<input
id="wf-name"
type="text"
className="tbk-input"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Send confirmation email"
required
/>
</div>
{/* Step 1: Trigger */}
<fieldset className="tbk-fieldset">
<legend className="tbk-legend">1. Trigger</legend>
<select
className="tbk-select"
value={trigger}
onChange={(e) => setTrigger(e.target.value)}
>
{triggers.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
</fieldset>
{/* Step 2: Conditions */}
<fieldset className="tbk-fieldset">
<legend className="tbk-legend">2. Conditions (optional)</legend>
{conditions.map((condition, idx) => (
<div key={idx} className="tbk-condition-row">
<select
className="tbk-select"
value={condition.field}
onChange={(e) =>
updateCondition(idx, { field: e.target.value })
}
>
{conditionFields.map((f) => (
<option key={f.value} value={f.value}>
{f.label}
</option>
))}
</select>
<select
className="tbk-select"
value={condition.operator}
onChange={(e) =>
updateCondition(idx, { operator: e.target.value })
}
>
<option value="equals">equals</option>
<option value="not_equals">not equals</option>
<option value="contains">contains</option>
</select>
<input
type="text"
className="tbk-input"
value={condition.value}
onChange={(e) =>
updateCondition(idx, { value: e.target.value })
}
placeholder="Value"
/>
<button
type="button"
className="tbk-button-danger"
onClick={() => removeCondition(idx)}
>
Remove
</button>
</div>
))}
<button
type="button"
className="tbk-button-secondary"
onClick={addCondition}
>
Add Condition
</button>
</fieldset>
{/* Step 3: Actions */}
<fieldset className="tbk-fieldset">
<legend className="tbk-legend">3. Actions</legend>
{actions.map((action, idx) => (
<div key={idx} className="tbk-action-card">
<div className="tbk-action-header">
<select
className="tbk-select"
value={action.type}
onChange={(e) =>
updateAction(idx, {
type: e.target.value,
config: {},
})
}
>
{actionTypes.map((at) => (
<option key={at.value} value={at.value}>
{at.label}
</option>
))}
</select>
{actions.length > 1 && (
<button
type="button"
className="tbk-button-danger"
onClick={() => removeAction(idx)}
>
Remove
</button>
)}
</div>
{/* Action-specific config fields */}
{action.type === "send_email" && (
<div className="tbk-action-config">
<input
type="text"
className="tbk-input"
placeholder="To (customer, host, or email)"
value={action.config.to ?? ""}
onChange={(e) =>
updateActionConfig(idx, "to", e.target.value)
}
/>
<input
type="text"
className="tbk-input"
placeholder="Subject"
value={action.config.subject ?? ""}
onChange={(e) =>
updateActionConfig(idx, "subject", e.target.value)
}
/>
<textarea
className="tbk-textarea"
placeholder="Body template (use {booking.title}, {attendee.name}, etc.)"
value={action.config.body ?? ""}
onChange={(e) =>
updateActionConfig(idx, "body", e.target.value)
}
/>
</div>
)}
{action.type === "send_sms" && (
<div className="tbk-action-config">
<input
type="text"
className="tbk-input"
placeholder="To (phone number or field key)"
value={action.config.to ?? ""}
onChange={(e) =>
updateActionConfig(idx, "to", e.target.value)
}
/>
<textarea
className="tbk-textarea"
placeholder="Message template"
value={action.config.body ?? ""}
onChange={(e) =>
updateActionConfig(idx, "body", e.target.value)
}
/>
</div>
)}
{action.type === "fire_webhook" && (
<div className="tbk-action-config">
<input
type="url"
className="tbk-input"
placeholder="Webhook URL"
value={action.config.url ?? ""}
onChange={(e) =>
updateActionConfig(idx, "url", e.target.value)
}
/>
</div>
)}
{action.type === "update_status" && (
<div className="tbk-action-config">
<select
className="tbk-select"
value={action.config.status ?? ""}
onChange={(e) =>
updateActionConfig(idx, "status", e.target.value)
}
>
<option value="">Select status...</option>
<option value="confirmed">Confirmed</option>
<option value="cancelled">Cancelled</option>
<option value="completed">Completed</option>
</select>
</div>
)}
</div>
))}
<button
type="button"
className="tbk-button-secondary"
onClick={addAction}
>
Add Action
</button>
</fieldset>
{/* Step 4: Active toggle */}
<div className="tbk-field">
<label className="tbk-checkbox-label">
<input
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
/>
<span>Active</span>
</label>
</div>
{/* Submit */}
<div className="tbk-form-actions">
<button type="submit" className="tbk-button-primary">
Save Workflow
</button>
{onCancel && (
<button
type="button"
className="tbk-button-secondary"
onClick={onCancel}
>
Cancel
</button>
)}
</div>
</form>
{/* Execution History */}
{executionLogs && executionLogs.length > 0 && (
<div className="tbk-workflow-logs">
<h4>Execution History</h4>
<table className="tbk-workflow-logs-table">
<thead>
<tr>
<th>Date</th>
<th>Action</th>
<th>Status</th>
<th>Error</th>
</tr>
</thead>
<tbody>
{executionLogs.map((log) => (
<tr key={log.id}>
<td>{log.executedAt.toLocaleString()}</td>
<td>{log.actionType}</td>
<td>
<span
className={cn(
"tbk-log-status",
`tbk-log-status-${log.status}`,
)}
>
{log.status}
</span>
</td>
<td>{log.error ?? ""}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}