@joshualelon/clawdbot-skill-flow
v2.4.1
Published
Multi-step workflow orchestration plugin for Clawdbot
Maintainers
Readme
Skill Flow
Multi-step workflow orchestration plugin for Clawdbot.
Build deterministic, button-driven conversation flows without AI inference overhead. Perfect for scheduled workouts, surveys, onboarding wizards, approval workflows, and any wizard-style interactions.
Features
- Deterministic Execution - Telegram button callbacks route directly to plugin commands, bypassing LLM entirely
- LLM-Powered Flow Generation - Generate complete flows from natural language descriptions
- Adaptive Step Modification - AI-powered hooks that personalize messages based on user context
- Multi-Step Workflows - Chain steps with conditional branching and variable capture
- Channel Rendering - Telegram inline keyboards with automatic fallback to text-based menus
- Input Validation - Built-in validators for numbers, emails, and phone numbers
- Variable Interpolation - Use
{{variableName}}in messages to display captured data - Session Management - Automatic timeout handling (30 minutes) with in-memory state
- History Tracking - JSONL append-only log for completed flows
- Cron Integration - Schedule flows to run automatically via Clawdbot's cron system
- Hooks Utility Library - Pre-built integrations for Google Sheets, dynamic buttons, scheduling, and more
Requirements
- Clawdbot: v2026.1.25 or later (requires Telegram
sendPayloadsupport)- PR: https://github.com/clawdbot/clawdbot/pull/1917
- Important: Telegram inline keyboard buttons will not work with older Clawdbot versions
- Text-based fallback will work on all versions
- Node.js: 22+ (same as Clawdbot)
- Channels: Currently optimized for Telegram (other channels use text-based menus)
Quick Start
1. Install Plugin
clawdbot plugins install @joshualelon/clawdbot-skill-flow2. Create a Flow
clawdbot message send "/flow_create import $(cat <<'EOF'
{
"name": "daily-checkin",
"description": "Daily wellness check-in",
"version": "1.0.0",
"steps": [
{
"id": "mood",
"message": "How are you feeling today?",
"buttons": ["😊 Great", "😐 Okay", "😔 Not great"],
"capture": "mood",
"next": "sleep"
},
{
"id": "sleep",
"message": "How many hours did you sleep?",
"buttons": [4, 5, 6, 7, 8, 9, 10],
"capture": "sleep",
"validate": "number"
}
]
}
EOF
)"3. Run the Flow
/flow_start daily-checkinConfiguration
The plugin supports several configuration options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| flowsDir | string | ~/.clawdbot/flows | Custom directory for flow definitions |
| sessionTimeoutMinutes | number | 30 | Session timeout (1-1440 minutes) |
| sessionCleanupIntervalMinutes | number | 5 | Cleanup check interval (1-60 minutes) |
| enableBuiltinHistory | boolean | true | Save completed flows to .jsonl files |
| maxFlowsPerUser | number | unlimited | Limit concurrent flows per user |
| llm.flowGenerationTimeout | number | 30000 | Flow generation timeout (ms) |
| llm.adaptationTimeout | number | 5000 | Step adaptation timeout (ms) |
| llm.maxTokens | number | 4096 | Max tokens for responses |
| llm.temperature | number | 0.7 | Response creativity (0-2) |
Note: LLM features use Clawdbot's Claude configuration automatically. Provider/model settings are inherited from Clawdbot.
Setting Configuration
# Use custom flows directory (e.g., for job tracking system)
clawdbot config set plugins.entries.clawdbot-skill-flow.config.flowsDir "~/clawd/jobs"
# Adjust session timeout to 60 minutes
clawdbot config set plugins.entries.clawdbot-skill-flow.config.sessionTimeoutMinutes 60
# Disable built-in history (if using custom storage backend)
clawdbot config set plugins.entries.clawdbot-skill-flow.config.enableBuiltinHistory false
# Restart gateway to apply changes
systemctl --user restart clawdbot-gateway # Linux
# or restart via Clawdbot Mac app menuCommands
| Command | Description | Example |
|---------|-------------|---------|
| /flow_start <name> | Start a flow | /flow_start pushups |
| /flow_list | List all flows | /flow_list |
| /flow_create import <json> | Create flow from JSON | See Quick Start |
| /flow_generate <description> | Generate flow from natural language (AI) | /flow_generate Create a mood tracker |
| /flow_delete <name> | Delete a flow | /flow_delete pushups |
| /flow_step | Internal command (callback handler) | N/A |
Example Flows
Pushups Workout
4-set pushup tracker with rep counting:
clawdbot message send "/flow_create import $(cat src/examples/pushups.json)"Customer Survey
Satisfaction survey with conditional branching (high vs low scores):
clawdbot message send "/flow_create import $(cat src/examples/survey.json)"Onboarding Wizard
Multi-step setup with email validation and variable interpolation:
clawdbot message send "/flow_create import $(cat src/examples/onboarding.json)"Flow Schema
See API Documentation for complete schema reference.
Minimal Example
{
"name": "hello-world",
"description": "Simple greeting flow",
"version": "1.0.0",
"steps": [
{
"id": "greeting",
"message": "What's your name?",
"capture": "name",
"next": "farewell"
},
{
"id": "farewell",
"message": "Nice to meet you, {{name}}! 👋"
}
]
}Cron Integration
Schedule flows to run automatically:
clawdbot cron add \
--name "daily-pushups" \
--schedule "45 13 * * *" \
--session-target isolated \
--message "/flow_start pushups" \
--channel telegram \
--to "+1234567890"Verify:
clawdbot cron listLLM-Powered Features
Skill Flow now supports AI-powered features that bridge deterministic execution with adaptive intelligence:
Flow Generation
Generate complete flows from natural language descriptions:
/flow_generate Create a 4-set pushup tracker with progressive difficultyThe AI will:
- Design appropriate steps and branching logic
- Add validation for captured data
- Create natural, conversational messages
- Structure the flow according to best practices
Preview before saving:
# Generate flow
/flow_generate Create a daily wellness check-in with mood and sleep tracking
# Review preview and JSON
# If satisfied, save it:
/flow_generate save
# Or cancel:
/flow_generate cancel
# Start the flow
/flow_start wellness-checkinAdaptive Step Modification
Make your flows adaptive with LLM-powered hooks that personalize messages based on user context:
// ~/.clawdbot/flows/pushups/hooks.js
export default (api) => {
const { createLLMAdapter } = api.hooks;
return {
onStepRender: createLLMAdapter(api, {
adaptMessage: true,
adaptButtons: true,
includeVariables: true
})
};
};What it does:
- Personalizes messages based on captured variables
- Adapts button labels to be more contextual
- Makes conversations feel natural and engaging
- Falls back gracefully if LLM unavailable
Example transformation:
Original: "Set 2: How many pushups?"
Adapted: "Nice work on those 25 reps! Ready for set 2?"Compose with other hooks:
export default (api) => {
const { composeHooks, createDynamicButtons, createLLMAdapter } = api.hooks;
return {
onStepRender: composeHooks(
// First: Generate button values from history
createDynamicButtons({
variable: 'reps',
strategy: 'centered'
}),
// Then: Adapt message and labels with AI
createLLMAdapter(api, {
adaptMessage: true,
adaptButtons: true,
preserveButtonValues: true
})
)
};
};Configuration:
LLM features use Clawdbot's configured Claude instance (no separate API keys needed). You can optionally adjust performance settings:
# Adjust timeouts (optional)
clawdbot config set plugins.entries.clawdbot-skill-flow.config.llm.flowGenerationTimeout 30000
clawdbot config set plugins.entries.clawdbot-skill-flow.config.llm.adaptationTimeout 5000
# Adjust creativity/token limits (optional)
clawdbot config set plugins.entries.clawdbot-skill-flow.config.llm.temperature 0.7
clawdbot config set plugins.entries.clawdbot-skill-flow.config.llm.maxTokens 4096Documentation:
- LLM Adapter API Reference - Complete configuration options
src/examples/llm-adapter.example.js- Usage examples
Advanced Features
Conditional Branching
Route users to different steps based on captured variables:
{
"id": "nps",
"message": "Rate us 0-10",
"buttons": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
"capture": "nps",
"validate": "number",
"condition": {
"variable": "nps",
"greaterThan": 7,
"next": "positive-feedback"
},
"next": "negative-feedback"
}Input Validation
Enforce data types with built-in validators:
"validate": "number"- Numeric input only"validate": "email"- Valid email format"validate": "phone"- Phone number format
Variable Interpolation
Display captured data in subsequent messages:
{
"id": "summary",
"message": "Thanks {{name}}! You scored {{nps}}/10."
}Button-Specific Routing
Override default next on a per-button basis:
{
"id": "confirm",
"message": "Continue?",
"buttons": [
{ "text": "Yes", "value": "yes", "next": "step2" },
{ "text": "No", "value": "no", "next": "cancel" }
]
}Step-Level Actions System
Skill-flow uses explicit step-level actions instead of implicit global hooks. This makes flows self-documenting, LLM-generatable, and easier to understand.
Actions in Flow JSON
Each step can declare what actions to execute:
{
"name": "pushups",
"description": "4-set pushup workout",
"version": "1.0.0",
"hooks": "./hooks.js",
"steps": [
{
"id": "set1",
"message": "Set 1: How many pushups?",
"actions": {
"fetch": {
"historicalAverage": "getHistoricalAverage"
},
"beforeRender": ["generateDynamicButtons"],
"afterCapture": ["logToSheets"]
},
"capture": "set1",
"validate": "number",
"next": "set2"
}
]
}Action Types:
fetch - Get data before rendering (returns variables to inject)
- Format:
{ "varName": "actionFunctionName" } - Example: Fetch historical workout data to calculate averages
- Format:
beforeRender - Modify step before display (returns modified step)
- Format:
["actionFunctionName", ...] - Example: Generate dynamic buttons based on fetched data
- Format:
afterCapture - Side effects after capturing variable
- Format:
["actionFunctionName", ...] - Example: Log to Google Sheets, send notifications
- Format:
Hooks File Structure
Action functions are exported as named exports from your hooks file. All actions receive an api parameter as their final argument - plugin utilities are available via api.hooks.
Note: No imports needed! Just destructure what you need from
api.hooks.
Action Signatures:
// Fetch actions - retrieve data before rendering
export async function myFetch(session: FlowSession, api: EnhancedPluginApi) {
const { querySheetHistory } = api.hooks;
return { variableName: value };
}
// BeforeRender actions - modify step before display
export async function myBeforeRender(step: FlowStep, session: FlowSession, api: EnhancedPluginApi) {
const { generateButtonRange } = api.hooks;
return { ...step, buttons: [...] };
}
// AfterCapture actions - side effects after capturing input
export async function myAfterCapture(variable: string, value: string | number, session: FlowSession, api: EnhancedPluginApi) {
const { appendToSheet } = api.hooks;
await appendToSheet('spreadsheet-id', { [variable]: value });
}
// Create a new spreadsheet (typically in a fetch action)
export async function createWorkoutLog(session, api) {
const { createSpreadsheet } = api.hooks;
const { spreadsheetId, spreadsheetUrl } = await createSpreadsheet({
title: `${session.flowName} Log - ${new Date().getFullYear()}`,
worksheetName: 'Sessions',
headers: ['timestamp', 'userId', 'set1', 'set2', 'set3', 'set4', 'total'],
folderId: process.env.GOOGLE_DRIVE_FOLDER_ID, // Optional: move to specific folder
useGogOAuth: true // Use gog CLI OAuth to avoid service account quota issues (requires GOG_ACCOUNT, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET env vars)
});
return { spreadsheetId, spreadsheetUrl };
}Complete Example:
// ~/.clawdbot/flows/pushups/hooks.js
/**
* Fetch action - returns variables to inject
*/
export async function getHistoricalAverage(session, api) {
// Access plugin utilities via api.hooks (no imports needed!)
const { querySheetHistory } = api.hooks;
const history = await querySheetHistory('spreadsheet-id', { limit: 10 });
const avg = history.reduce((sum, row) => sum + row.reps, 0) / history.length;
return { historicalAverage: Math.round(avg) };
}
/**
* BeforeRender action - returns modified step
*/
export async function generateDynamicButtons(step, session, api) {
const { generateButtonRange } = api.hooks;
const avg = session.variables.historicalAverage || 25;
return {
...step,
buttons: generateButtonRange(avg, { count: 5, spread: 5 })
};
}
/**
* AfterCapture action - side effects only
*/
export async function logToSheets(variable, value, session, api) {
const { appendToSheet } = api.hooks;
await appendToSheet('spreadsheet-id', {
date: new Date().toISOString(),
[variable]: value
});
}
/**
* Global lifecycle hooks (default export)
* Use factory function to access api.hooks
*/
export default (api) => {
const { createClawdBotScheduler } = api.hooks;
const scheduler = createClawdBotScheduler(api);
return {
async onFlowComplete(session) {
console.log('Workout complete!', session.variables);
// Schedule next session using plugin utilities
await scheduler.schedule({
name: `${session.flowName}-next`,
schedule: '0 9 * * *', // Daily at 9am
message: `/flow_start ${session.flowName}`,
});
},
async onFlowAbandoned(session, reason) {
console.log('Flow abandoned:', reason);
}
};
};See the Hooks API Reference for complete documentation of available utilities.
Benefits of Step-Level Actions
Before (Implicit Global Hooks):
// Flow JSON doesn't show what happens
{ "id": "set1", "capture": "set1" }
// Hook has to filter by step ID
export default {
async onStepRender(step, session) {
if (step.id === 'set1') {
// Hidden logic...
}
}
}After (Explicit Step-Level Actions):
{
"id": "set1",
"actions": {
"fetch": { "avg": "getAverage" },
"beforeRender": ["generateButtons"],
"afterCapture": ["logToSheets"]
}
}Benefits:
- ✅ Self-documenting (JSON shows what happens)
- ✅ LLM-generatable (AI can see available actions)
- ✅ Git-friendly (changes visible in JSON diffs)
- ✅ Testable (test actions in isolation)
- ✅ No step.id filtering needed
Complete Example
See src/examples/pushups/ for a complete working example with:
- Fetch actions for historical data
- Dynamic button generation
- Google Sheets logging (mock)
- Lifecycle hooks for scheduling
Hooks Utility Library
The plugin includes pre-built integrations (work in progress):
- Google Sheets - Log flow data and query history
- Dynamic Buttons - Generate buttons based on historical data
- Scheduling - Schedule recurring workflow sessions
- Common Utilities - Compose actions, retry logic, validation
Note: The utility library is being updated to work with the new step-level actions system.
Documentation:
- Hooks & Actions Reference - Complete API documentation
src/examples/pushups/- Complete working examplesrc/examples/survey/- Survey with conditional actions
Custom Storage Backends
Replace or supplement the built-in JSONL storage:
{
"name": "pushups",
"storage": {
"backend": "./storage.js",
"builtin": false
},
"steps": [...]
}StorageBackend Interface:
export default {
async saveSession(session) {
// Write to Google Sheets, database, etc.
},
async loadHistory(flowName, options) {
// Return historical sessions for analytics
return [];
},
};Set "builtin": false to disable JSONL storage and only use the custom backend. Omit it (or set to true) to use both.
See src/examples/sheets-storage.example.js for a complete reference.
Security
Hooks & Storage Backend Safety
The plugin validates that all dynamically loaded files (hooks, storage backends) remain within the ~/.clawdbot/flows/ directory. This prevents directory traversal attacks.
Valid hook paths:
{
"name": "myflow",
"hooks": "./hooks.js", // ✅ Relative to flow directory
"hooks": "hooks/custom.js" // ✅ Subdirectory within flow
}Invalid hook paths (will be rejected):
{
"hooks": "/etc/passwd", // ❌ Absolute path outside flows
"hooks": "../../../etc/passwd", // ❌ Directory traversal
"hooks": "~/malicious.js" // ❌ Tilde expansion outside flows
}The plugin uses path validation similar to Clawdbot's core security patterns to ensure hooks and storage backends can only access files within their designated flow directory.
How It Works
Telegram Button Callbacks
When you render a Telegram inline keyboard button:
{
text: "20",
callback_data: "/flow-step pushups set1:20"
}Telegram sends the callback_data string back to Clawdbot, which routes it directly to the /flow-step command—no LLM inference required.
This enables deterministic, instant responses for structured workflows.
Session Management
- Sessions stored in-memory with 30-minute timeout
- Session key:
${senderId}-${flowName} - Automatic cleanup every 5 minutes
- History saved to
~/.clawdbot/flows/<name>/history.jsonlon completion
File Structure
~/.clawdbot/flows/
├── pushups/
│ ├── metadata.json
│ └── history.jsonl
└── survey/
├── metadata.json
└── history.jsonlTroubleshooting
Plugin Failed to Load - Missing Dependencies
Symptom:
Error: Cannot find module 'zod'
Plugin "clawdbot-skill-flow" failed to loadCause: The clawdbot plugins install command timed out during npm install, leaving node_modules partially corrupted.
Fix:
# 1. Find your plugin directory
cd ~/.clawdbot/extensions/@joshualelon/clawdbot-skill-flow
# 2. Clean and reinstall dependencies
rm -rf node_modules package-lock.json
npm install --omit=optional
# 3. Restart gateway
systemctl --user restart clawdbot-gateway # Linux
# OR
# Restart from Clawdbot menu bar app (macOS)Plugin Install Timeout
Symptom:
npm install timed out
Plugin installation incompleteCause: Large dependency tree or slow network connection.
Solutions:
Manual install:
cd ~/.clawdbot/extensions/@joshualelon/clawdbot-skill-flow npm install --timeout=120000Use faster registry (optional):
npm config set registry https://registry.npmjs.org/ npm installSkip optional dependencies:
npm install --omit=optional --no-audit
Flow Not Found
Symptom:
Flow "my-flow" not foundSolutions:
Check flows directory:
ls ~/.clawdbot/flows/ # Or your custom flowsDirVerify flow created:
/flow_listCheck file permissions:
chmod 644 ~/.clawdbot/flows/my-flow/metadata.json
Telegram Buttons Not Working
Symptom: Buttons don't appear or don't respond when clicked.
Cause: Clawdbot version doesn't support sendPayload.
Solution: Upgrade Clawdbot to v2026.1.25+ which includes PR #1917.
Text-based menus will work as fallback on older versions.
Session Timeout Issues
Symptom: "Session expired" messages appear too quickly.
Solution: Increase timeout in plugin config:
clawdbot config set plugins.entries.clawdbot-skill-flow.config.sessionTimeoutMinutes 60Hook Execution Timeout
Symptom:
Action "myAction" timed out after 5000msSolutions:
Increase action timeout:
clawdbot config set plugins.entries.clawdbot-skill-flow.config.security.actionTimeout 10000Optimize slow hooks: Move heavy computation to background jobs.
Use conditional execution:
{ "actions": { "fetch": { "data": { "action": "fetchData", "if": "needsData" } } } }
LLM Features Not Working
Symptom: /flow_generate or LLM adapters fail.
Cause: No Claude API configured in Clawdbot.
Solution: Configure Claude in Clawdbot settings. The plugin inherits Clawdbot's LLM configuration automatically.
Debugging Tips
Check plugin logs:
journalctl --user -u clawdbot-gateway -f # Linux # OR tail -f ~/Library/Logs/Clawdbot/gateway.log # macOSEnable debug logging:
clawdbot config set plugins.entries.clawdbot-skill-flow.config.debug trueTest flow JSON manually:
cat ~/.clawdbot/flows/my-flow/metadata.json | jq .Verify plugin loaded:
/flow_list # Should list available flows
Contributing
Issues and PRs welcome! This plugin follows Clawdbot's coding conventions.
License
MIT - See LICENSE file
