@joshualelon/clawdbot-skill-flow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/clawdbot.plugin.json +8 -0
- package/index.ts +65 -0
- package/package.json +76 -0
- package/src/commands/flow-create.ts +69 -0
- package/src/commands/flow-delete.ts +36 -0
- package/src/commands/flow-list.ts +33 -0
- package/src/commands/flow-start.ts +56 -0
- package/src/commands/flow-step.ts +101 -0
- package/src/engine/executor.ts +109 -0
- package/src/engine/renderer.ts +119 -0
- package/src/engine/transitions.ts +160 -0
- package/src/examples/onboarding.json +41 -0
- package/src/examples/pushups.json +49 -0
- package/src/examples/survey.json +41 -0
- package/src/state/flow-store.ts +158 -0
- package/src/state/history-store.ts +76 -0
- package/src/state/session-store.ts +155 -0
- package/src/types.ts +67 -0
- package/src/validation.ts +119 -0
- package/types.d.ts +36 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /flow-step command - Handle flow step transitions (called via Telegram callbacks)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
6
|
+
import { loadFlow } from "../state/flow-store.js";
|
|
7
|
+
import {
|
|
8
|
+
getSession,
|
|
9
|
+
getSessionKey,
|
|
10
|
+
updateSession,
|
|
11
|
+
deleteSession,
|
|
12
|
+
} from "../state/session-store.js";
|
|
13
|
+
import { saveFlowHistory } from "../state/history-store.js";
|
|
14
|
+
import { processStep } from "../engine/executor.js";
|
|
15
|
+
|
|
16
|
+
export function createFlowStepCommand(api: ClawdbotPluginApi) {
|
|
17
|
+
return async (args: {
|
|
18
|
+
args?: string;
|
|
19
|
+
senderId: string;
|
|
20
|
+
channel: string;
|
|
21
|
+
}) => {
|
|
22
|
+
const input = args.args?.trim();
|
|
23
|
+
|
|
24
|
+
if (!input) {
|
|
25
|
+
return {
|
|
26
|
+
text: "Error: Missing step parameters",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parse callback: "flowName stepId:value"
|
|
31
|
+
const parts = input.split(" ");
|
|
32
|
+
if (parts.length < 2) {
|
|
33
|
+
return {
|
|
34
|
+
text: "Error: Invalid step parameters",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const flowName = parts[0]!;
|
|
39
|
+
const stepData = parts.slice(1).join(" "); // Handle spaces in value
|
|
40
|
+
const colonIndex = stepData.indexOf(":");
|
|
41
|
+
|
|
42
|
+
if (colonIndex === -1) {
|
|
43
|
+
return {
|
|
44
|
+
text: "Error: Invalid step format (expected stepId:value)",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stepId = stepData.substring(0, colonIndex);
|
|
49
|
+
const valueStr = stepData.substring(colonIndex + 1);
|
|
50
|
+
|
|
51
|
+
// Get active session
|
|
52
|
+
const sessionKey = getSessionKey(args.senderId, flowName);
|
|
53
|
+
const session = getSession(sessionKey);
|
|
54
|
+
|
|
55
|
+
if (!session) {
|
|
56
|
+
return {
|
|
57
|
+
text: `Session expired or not found.\n\nUse /flow-start ${flowName} to restart the flow.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Load flow
|
|
62
|
+
const flow = await loadFlow(api, flowName);
|
|
63
|
+
|
|
64
|
+
if (!flow) {
|
|
65
|
+
deleteSession(sessionKey);
|
|
66
|
+
return {
|
|
67
|
+
text: `Flow "${flowName}" not found.`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Detect if value should be numeric
|
|
72
|
+
const value = /^\d+$/.test(valueStr) ? Number(valueStr) : valueStr;
|
|
73
|
+
|
|
74
|
+
// Process step transition
|
|
75
|
+
const result = processStep(api, flow, session, stepId, value);
|
|
76
|
+
|
|
77
|
+
// Update session or cleanup
|
|
78
|
+
if (result.complete) {
|
|
79
|
+
// Save to history
|
|
80
|
+
const finalSession = {
|
|
81
|
+
...session,
|
|
82
|
+
variables: result.updatedVariables,
|
|
83
|
+
};
|
|
84
|
+
await saveFlowHistory(api, finalSession);
|
|
85
|
+
|
|
86
|
+
// Cleanup session
|
|
87
|
+
deleteSession(sessionKey);
|
|
88
|
+
|
|
89
|
+
api.logger.info(
|
|
90
|
+
`Completed flow "${flowName}" for user ${args.senderId}`
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
// Update session with new variables and current step
|
|
94
|
+
updateSession(sessionKey, {
|
|
95
|
+
variables: result.updatedVariables,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result.reply;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main flow execution orchestrator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
6
|
+
import type { FlowMetadata, FlowSession, ReplyPayload } from "../types.js";
|
|
7
|
+
import { renderStep } from "./renderer.js";
|
|
8
|
+
import { executeTransition } from "./transitions.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Start a flow from the beginning
|
|
12
|
+
*/
|
|
13
|
+
export function startFlow(
|
|
14
|
+
api: ClawdbotPluginApi,
|
|
15
|
+
flow: FlowMetadata,
|
|
16
|
+
session: FlowSession
|
|
17
|
+
): ReplyPayload {
|
|
18
|
+
const firstStep = flow.steps[0];
|
|
19
|
+
|
|
20
|
+
if (!firstStep) {
|
|
21
|
+
return {
|
|
22
|
+
text: `Error: Flow "${flow.name}" has no steps`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return renderStep(api, flow, firstStep, session, session.channel);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Process a step transition and return next step or completion message
|
|
31
|
+
*/
|
|
32
|
+
export function processStep(
|
|
33
|
+
api: ClawdbotPluginApi,
|
|
34
|
+
flow: FlowMetadata,
|
|
35
|
+
session: FlowSession,
|
|
36
|
+
stepId: string,
|
|
37
|
+
value: string | number
|
|
38
|
+
): {
|
|
39
|
+
reply: ReplyPayload;
|
|
40
|
+
complete: boolean;
|
|
41
|
+
updatedVariables: Record<string, string | number>;
|
|
42
|
+
} {
|
|
43
|
+
const result = executeTransition(api, flow, session, stepId, value);
|
|
44
|
+
|
|
45
|
+
// Handle errors
|
|
46
|
+
if (result.error) {
|
|
47
|
+
return {
|
|
48
|
+
reply: { text: result.message || result.error },
|
|
49
|
+
complete: false,
|
|
50
|
+
updatedVariables: result.variables,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Handle completion
|
|
55
|
+
if (result.complete) {
|
|
56
|
+
const completionMessage = generateCompletionMessage(flow, result.variables);
|
|
57
|
+
return {
|
|
58
|
+
reply: { text: completionMessage },
|
|
59
|
+
complete: true,
|
|
60
|
+
updatedVariables: result.variables,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Render next step
|
|
65
|
+
if (!result.nextStepId) {
|
|
66
|
+
return {
|
|
67
|
+
reply: { text: "Error: No next step found" },
|
|
68
|
+
complete: false,
|
|
69
|
+
updatedVariables: result.variables,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const nextStep = flow.steps.find((s) => s.id === result.nextStepId);
|
|
74
|
+
if (!nextStep) {
|
|
75
|
+
return {
|
|
76
|
+
reply: { text: `Error: Step ${result.nextStepId} not found` },
|
|
77
|
+
complete: false,
|
|
78
|
+
updatedVariables: result.variables,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const updatedSession = { ...session, variables: result.variables };
|
|
83
|
+
const reply = renderStep(api, flow, nextStep, updatedSession, session.channel);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
reply,
|
|
87
|
+
complete: false,
|
|
88
|
+
updatedVariables: result.variables,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate completion message
|
|
94
|
+
*/
|
|
95
|
+
function generateCompletionMessage(
|
|
96
|
+
flow: FlowMetadata,
|
|
97
|
+
variables: Record<string, string | number>
|
|
98
|
+
): string {
|
|
99
|
+
let message = `ā
Flow "${flow.name}" completed!\n\n`;
|
|
100
|
+
|
|
101
|
+
if (Object.keys(variables).length > 0) {
|
|
102
|
+
message += "Summary:\n";
|
|
103
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
104
|
+
message += `⢠${key}: ${value}\n`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return message;
|
|
109
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow step rendering for different channel types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
6
|
+
import type {
|
|
7
|
+
FlowMetadata,
|
|
8
|
+
FlowStep,
|
|
9
|
+
FlowSession,
|
|
10
|
+
ReplyPayload,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { normalizeButton } from "../validation.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Interpolate variables in message text
|
|
16
|
+
*/
|
|
17
|
+
function interpolateVariables(
|
|
18
|
+
text: string,
|
|
19
|
+
variables: Record<string, string | number>
|
|
20
|
+
): string {
|
|
21
|
+
return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
|
|
22
|
+
const value = variables[varName];
|
|
23
|
+
return value !== undefined ? String(value) : match;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render step for Telegram (with inline keyboard)
|
|
29
|
+
*/
|
|
30
|
+
function renderTelegram(
|
|
31
|
+
flowName: string,
|
|
32
|
+
step: FlowStep,
|
|
33
|
+
variables: Record<string, string | number>
|
|
34
|
+
): ReplyPayload {
|
|
35
|
+
const message = interpolateVariables(step.message, variables);
|
|
36
|
+
|
|
37
|
+
if (!step.buttons || step.buttons.length === 0) {
|
|
38
|
+
return { text: message };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const buttons = step.buttons.map((btn, idx) =>
|
|
42
|
+
normalizeButton(btn, idx)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Detect if all buttons are numeric (for grid layout)
|
|
46
|
+
const allNumeric = buttons.every((btn) => typeof btn.value === "number");
|
|
47
|
+
|
|
48
|
+
let keyboard: Array<{ text: string; callback_data: string }[]>;
|
|
49
|
+
|
|
50
|
+
if (allNumeric && buttons.length > 2) {
|
|
51
|
+
// 2-column grid for numeric buttons
|
|
52
|
+
keyboard = [];
|
|
53
|
+
for (let i = 0; i < buttons.length; i += 2) {
|
|
54
|
+
const row = buttons.slice(i, i + 2).map((btn) => ({
|
|
55
|
+
text: btn.text,
|
|
56
|
+
callback_data: `/flow-step ${flowName} ${step.id}:${btn.value}`,
|
|
57
|
+
}));
|
|
58
|
+
keyboard.push(row);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// Single column for text buttons
|
|
62
|
+
keyboard = buttons.map((btn) => [
|
|
63
|
+
{
|
|
64
|
+
text: btn.text,
|
|
65
|
+
callback_data: `/flow-step ${flowName} ${step.id}:${btn.value}`,
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
text: message,
|
|
72
|
+
buttons: keyboard,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Render step for fallback channels (text-based)
|
|
78
|
+
*/
|
|
79
|
+
function renderFallback(
|
|
80
|
+
step: FlowStep,
|
|
81
|
+
variables: Record<string, string | number>
|
|
82
|
+
): ReplyPayload {
|
|
83
|
+
const message = interpolateVariables(step.message, variables);
|
|
84
|
+
|
|
85
|
+
if (!step.buttons || step.buttons.length === 0) {
|
|
86
|
+
return { text: message };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const buttons = step.buttons.map((btn, idx) =>
|
|
90
|
+
normalizeButton(btn, idx)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const buttonList = buttons
|
|
94
|
+
.map((btn, idx) => `${idx + 1}. ${btn.text}`)
|
|
95
|
+
.join("\n");
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
text: `${message}\n\n${buttonList}\n\nReply with the number of your choice.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Render a flow step
|
|
104
|
+
*/
|
|
105
|
+
export function renderStep(
|
|
106
|
+
_api: ClawdbotPluginApi,
|
|
107
|
+
flow: FlowMetadata,
|
|
108
|
+
step: FlowStep,
|
|
109
|
+
session: FlowSession,
|
|
110
|
+
channel: string
|
|
111
|
+
): ReplyPayload {
|
|
112
|
+
// Channel-specific rendering
|
|
113
|
+
if (channel === "telegram") {
|
|
114
|
+
return renderTelegram(flow.name, step, session.variables);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fallback for all other channels
|
|
118
|
+
return renderFallback(step, session.variables);
|
|
119
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow step transition logic with validation and conditional branching
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
6
|
+
import type {
|
|
7
|
+
FlowMetadata,
|
|
8
|
+
FlowStep,
|
|
9
|
+
FlowSession,
|
|
10
|
+
TransitionResult,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { normalizeButton, validateInput } from "../validation.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Evaluate condition against session variables
|
|
16
|
+
*/
|
|
17
|
+
function evaluateCondition(
|
|
18
|
+
condition: FlowStep["condition"],
|
|
19
|
+
variables: Record<string, string | number>
|
|
20
|
+
): boolean {
|
|
21
|
+
if (!condition) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const value = variables[condition.variable];
|
|
26
|
+
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check equals
|
|
32
|
+
if (condition.equals !== undefined) {
|
|
33
|
+
return value === condition.equals;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check greater than
|
|
37
|
+
if (condition.greaterThan !== undefined) {
|
|
38
|
+
const numValue =
|
|
39
|
+
typeof value === "number" ? value : Number(value);
|
|
40
|
+
return !isNaN(numValue) && numValue > condition.greaterThan;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check less than
|
|
44
|
+
if (condition.lessThan !== undefined) {
|
|
45
|
+
const numValue =
|
|
46
|
+
typeof value === "number" ? value : Number(value);
|
|
47
|
+
return !isNaN(numValue) && numValue < condition.lessThan;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check contains (for strings)
|
|
51
|
+
if (condition.contains !== undefined) {
|
|
52
|
+
return String(value).includes(condition.contains);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find next step ID based on transition rules
|
|
60
|
+
*/
|
|
61
|
+
function findNextStep(
|
|
62
|
+
step: FlowStep,
|
|
63
|
+
value: string | number,
|
|
64
|
+
variables: Record<string, string | number>
|
|
65
|
+
): string | undefined {
|
|
66
|
+
// 1. Check button-specific next
|
|
67
|
+
if (step.buttons && step.buttons.length > 0) {
|
|
68
|
+
const buttons = step.buttons.map((btn, idx) =>
|
|
69
|
+
normalizeButton(btn, idx)
|
|
70
|
+
);
|
|
71
|
+
const matchingButton = buttons.find((btn) => btn.value === value);
|
|
72
|
+
if (matchingButton?.next) {
|
|
73
|
+
return matchingButton.next;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Check conditional branching
|
|
78
|
+
if (step.condition && evaluateCondition(step.condition, variables)) {
|
|
79
|
+
return step.condition.next;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. Use default next
|
|
83
|
+
return step.next;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Execute step transition
|
|
88
|
+
*/
|
|
89
|
+
export function executeTransition(
|
|
90
|
+
_api: ClawdbotPluginApi,
|
|
91
|
+
flow: FlowMetadata,
|
|
92
|
+
session: FlowSession,
|
|
93
|
+
stepId: string,
|
|
94
|
+
value: string | number
|
|
95
|
+
): TransitionResult {
|
|
96
|
+
// Find current step
|
|
97
|
+
const step = flow.steps.find((s) => s.id === stepId);
|
|
98
|
+
|
|
99
|
+
if (!step) {
|
|
100
|
+
return {
|
|
101
|
+
variables: session.variables,
|
|
102
|
+
complete: false,
|
|
103
|
+
error: `Step ${stepId} not found`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Convert value to string for validation
|
|
108
|
+
const valueStr = String(value);
|
|
109
|
+
|
|
110
|
+
// Validate input if required
|
|
111
|
+
if (step.validate) {
|
|
112
|
+
const validation = validateInput(valueStr, step.validate);
|
|
113
|
+
if (!validation.valid) {
|
|
114
|
+
return {
|
|
115
|
+
variables: session.variables,
|
|
116
|
+
complete: false,
|
|
117
|
+
error: validation.error,
|
|
118
|
+
message: validation.error,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Capture variable if specified
|
|
124
|
+
const updatedVariables = { ...session.variables };
|
|
125
|
+
if (step.capture) {
|
|
126
|
+
// Convert to number if validation type is number
|
|
127
|
+
if (step.validate === "number") {
|
|
128
|
+
updatedVariables[step.capture] = Number(value);
|
|
129
|
+
} else {
|
|
130
|
+
updatedVariables[step.capture] = valueStr;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Find next step
|
|
135
|
+
const nextStepId = findNextStep(step, value, updatedVariables);
|
|
136
|
+
|
|
137
|
+
// If no next step, flow is complete
|
|
138
|
+
if (!nextStepId) {
|
|
139
|
+
return {
|
|
140
|
+
variables: updatedVariables,
|
|
141
|
+
complete: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Verify next step exists
|
|
146
|
+
const nextStep = flow.steps.find((s) => s.id === nextStepId);
|
|
147
|
+
if (!nextStep) {
|
|
148
|
+
return {
|
|
149
|
+
variables: updatedVariables,
|
|
150
|
+
complete: false,
|
|
151
|
+
error: `Next step ${nextStepId} not found`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
nextStepId,
|
|
157
|
+
variables: updatedVariables,
|
|
158
|
+
complete: false,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "onboarding",
|
|
3
|
+
"description": "User onboarding wizard with email validation",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": "Clawdbot Team",
|
|
6
|
+
"triggers": {
|
|
7
|
+
"manual": true
|
|
8
|
+
},
|
|
9
|
+
"steps": [
|
|
10
|
+
{
|
|
11
|
+
"id": "welcome",
|
|
12
|
+
"message": "š Welcome! Let's get you set up.\n\nWhat's your name?",
|
|
13
|
+
"capture": "name",
|
|
14
|
+
"next": "preferences"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "preferences",
|
|
18
|
+
"message": "Nice to meet you, {{name}}!\n\nWhat are you interested in?",
|
|
19
|
+
"buttons": [
|
|
20
|
+
"Productivity",
|
|
21
|
+
"Fitness",
|
|
22
|
+
"Learning",
|
|
23
|
+
"Entertainment"
|
|
24
|
+
],
|
|
25
|
+
"capture": "interests",
|
|
26
|
+
"next": "email"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "email",
|
|
30
|
+
"message": "What's your email address?",
|
|
31
|
+
"capture": "email",
|
|
32
|
+
"validate": "email",
|
|
33
|
+
"next": "confirm"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "confirm",
|
|
37
|
+
"message": "Perfect! Here's what we have:\n\n⢠Name: {{name}}\n⢠Interests: {{interests}}\n⢠Email: {{email}}\n\nReady to get started?",
|
|
38
|
+
"buttons": ["Yes, let's go!", "Start over"]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pushups",
|
|
3
|
+
"description": "4-set pushup workout tracker",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": "Clawdbot Team",
|
|
6
|
+
"triggers": {
|
|
7
|
+
"manual": true,
|
|
8
|
+
"cron": "45 13 * * *"
|
|
9
|
+
},
|
|
10
|
+
"steps": [
|
|
11
|
+
{
|
|
12
|
+
"id": "reminder",
|
|
13
|
+
"message": "šļø Time for your daily pushup workout!\n\nReady to start?",
|
|
14
|
+
"buttons": ["Yes, let's go!", "Skip today"],
|
|
15
|
+
"next": "set1"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "set1",
|
|
19
|
+
"message": "Set 1: How many pushups did you do?",
|
|
20
|
+
"buttons": [20, 25, 30, 35, 40],
|
|
21
|
+
"capture": "set1",
|
|
22
|
+
"validate": "number",
|
|
23
|
+
"next": "set2"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "set2",
|
|
27
|
+
"message": "Great! Set 2: How many pushups?",
|
|
28
|
+
"buttons": [20, 25, 30, 35, 40],
|
|
29
|
+
"capture": "set2",
|
|
30
|
+
"validate": "number",
|
|
31
|
+
"next": "set3"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "set3",
|
|
35
|
+
"message": "Awesome! Set 3: How many?",
|
|
36
|
+
"buttons": [20, 25, 30, 35, 40],
|
|
37
|
+
"capture": "set3",
|
|
38
|
+
"validate": "number",
|
|
39
|
+
"next": "set4"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "set4",
|
|
43
|
+
"message": "Final set! How many pushups?",
|
|
44
|
+
"buttons": [20, 25, 30, 35, 40],
|
|
45
|
+
"capture": "set4",
|
|
46
|
+
"validate": "number"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "survey",
|
|
3
|
+
"description": "Customer satisfaction survey with conditional branching",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": "Clawdbot Team",
|
|
6
|
+
"triggers": {
|
|
7
|
+
"manual": true
|
|
8
|
+
},
|
|
9
|
+
"steps": [
|
|
10
|
+
{
|
|
11
|
+
"id": "satisfaction",
|
|
12
|
+
"message": "How satisfied are you with our service?",
|
|
13
|
+
"buttons": ["ā", "āā", "āāā", "āāāā", "āāāāā"],
|
|
14
|
+
"capture": "satisfaction",
|
|
15
|
+
"next": "nps"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "nps",
|
|
19
|
+
"message": "On a scale of 0-10, how likely are you to recommend us?",
|
|
20
|
+
"buttons": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
21
|
+
"capture": "nps",
|
|
22
|
+
"validate": "number",
|
|
23
|
+
"condition": {
|
|
24
|
+
"variable": "nps",
|
|
25
|
+
"greaterThan": 7,
|
|
26
|
+
"next": "positive-feedback"
|
|
27
|
+
},
|
|
28
|
+
"next": "negative-feedback"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "positive-feedback",
|
|
32
|
+
"message": "Great to hear! š What did you love most about our service?",
|
|
33
|
+
"capture": "feedback"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "negative-feedback",
|
|
37
|
+
"message": "We're sorry to hear that. What can we improve?",
|
|
38
|
+
"capture": "feedback"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|