@candoa/workflows
v0.1.0
Published
Type-safe workflow SDK for Candoa with Copilot-friendly syntax. Define chat workflows with triggers, actions, and type safety.
Maintainers
Readme
@candoa/workflows
Self-contained, type-safe workflow SDK for Candoa with Copilot-friendly syntax. Define chat workflows with triggers, actions, and full TypeScript support - no external dependencies required.
🚀 Features
- Copilot-friendly syntax - Optimized for GitHub Copilot autocompletion
- Type-safe - Full TypeScript support with parameter validation
- Builder pattern - Fluent API with method chaining
- Template variables - Dynamic value substitution with
{{variableName}} - Pre-built actions - Common workflow actions ready to use
- Self-contained - No external dependencies, all types included
- Database integration - Seamless integration with Candoa CRM
- Sync command - Deploy workflows to database with
pnpm run sync-workflows
📦 Installation
npm install @candoa/workflows
# or
pnpm add @candoa/workflows🔥 Quick Start
import { workflow, collectFirstName, saveToContact } from '@candoa/workflows'
export default workflow('welcome-new-user', {
description: "Collects the user's first name when they start a chat",
})
.on('chat.started')
.use(
collectFirstName({ prompt: "Hi! What's your first name?" }),
saveToContact({ field: 'firstName', value: '{{firstName}}' }),
)🧠 Core Concepts
Workflow Definition
A workflow consists of:
- Name - Unique identifier for the workflow
- Description - Optional documentation
- Trigger - Event that starts the workflow
- Actions - Sequence of operations to execute
Template Variables
Use template variables to pass data between actions:
workflow('collect-contact-info')
.on('chat.started')
.use(
collectFirstName({ prompt: "What's your first name?" }),
collectEmail({ prompt: "What's your email address?" }),
saveToContact({ field: 'firstName', value: '{{firstName}}' }),
saveToContact({ field: 'email', value: '{{email}}' }),
sendMessage({
message: "Thanks {{firstName}}! I've saved your information.",
}),
)🎯 Triggers
Available trigger types with full TypeScript autocompletion:
chat.started- When a new chat session beginschat.ended- When a chat session endsmessage.received- When user sends a messageintent.detected- When AI detects specific intentsentiment.negative- When negative sentiment is detectedpayment.failed- When a payment failsuser.signup- When a user signs upcustom.event- For custom events
⚡ Actions
Data Collection
// Collect user information
collectFirstName({ prompt: "What's your first name?" })
collectLastName({ prompt: "What's your last name?" })
collectEmail({ prompt: "What's your email address?" })
// Save to contact record
saveToContact({ field: 'firstName', value: '{{firstName}}' })
saveToContact({ field: 'email', value: '{{email}}' })Communication
// Send messages
sendMessage({ message: 'Hello {{firstName}}!' })
sendMessage({ message: 'Please wait...', delay: 2000 })
// Send emails
sendEmail({
to: '{{email}}',
subject: 'Welcome to our platform!',
body: 'Hi {{firstName}}, welcome aboard!',
})Workflow Control
// Wait for a period
wait({ duration: 5, unit: 'seconds' })
// Conditional logic
conditional({
condition: "{{sentiment}} == 'negative'",
trueAction: handoffToHuman({ reason: 'Negative sentiment detected' }),
falseAction: sendMessage({ message: 'How else can I help you?' }),
})
// Hand off to human agent
handoffToHuman({
reason: 'Complex issue requiring human assistance',
})Task Management
// Create tasks
createTask({
title: 'Follow up with {{firstName}}',
description: 'User expressed interest in premium features',
})
// Add tags
addTags({
tags: ['new-user', 'interested'],
target: 'contact',
})🗄️ Database Integration
Workflow Storage
Workflows are stored in your workflows table:
CREATE TABLE "workflow" (
"id" text PRIMARY KEY,
"projectId" text NOT NULL REFERENCES "project"("id"),
"name" text NOT NULL,
"workflowData" json NOT NULL, -- SDK data stored here
"createdAt" timestamp DEFAULT now(),
"updatedAt" timestamp DEFAULT now()
);Manual Database Integration
import {
prepareWorkflowForDatabase,
validateWorkflowCompatibility,
} from '@candoa/workflows'
// 1. Define your workflow
const welcomeWorkflow = workflow('welcome-new-user', {
description: "Collects the user's first name when they start a chat",
})
.on('chat.started')
.use(
collectFirstName({ prompt: "Hi! What's your first name?" }),
saveToContact({ field: 'firstName', value: '{{firstName}}' }),
)
// 2. Validate workflow
const validation = validateWorkflowCompatibility(welcomeWorkflow)
if (!validation.isValid) {
console.error('Validation errors:', validation.errors)
return
}
// 3. Prepare for database
const dbData = prepareWorkflowForDatabase(welcomeWorkflow, projectId)
// 4. Insert into database
await db.insert(workflows).values({
id: crypto.randomUUID(),
name: dbData.name,
projectId: dbData.projectId,
workflowData: dbData.workflowData,
})🔄 Workflow Sync System
Quick Sync
Create workflows in files and sync them to your database:
# Sync all workflows to your project
pnpm run sync-workflows --project-id=your-project-uuid
# Preview changes without applying them
pnpm run sync-workflows --project-id=your-project-uuid --dry-run
# Force overwrite existing workflows
pnpm run sync-workflows --project-id=your-project-uuid --forceDirectory Structure
The sync script discovers workflows in these locations:
packages/workflows/
├── examples/ # 📁 Example workflows (use as templates)
│ ├── welcome.ts
│ ├── candoa-integration.ts
│ └── advanced-workflows.ts
└── scripts/
└── sync-workflows.ts # 🔧 Sync scriptWorkflow File Format
Export workflows from your files:
// workflows/my-workflows.ts
import { workflow, sendMessage, collectFirstName } from '@candoa/workflows'
// ✅ Default export
export default workflow('welcome', {
description: 'Welcome new users',
})
.on('chat.started')
.use(
sendMessage({ message: 'Welcome!' }),
collectFirstName({ prompt: "What's your name?" }),
)
// ✅ Named exports
export const supportFlow = workflow('support-flow', {
description: 'Handle support requests',
})
.on('intent.detected')
.use(sendMessage({ message: 'I can help with that!' }))Sync Output
🔄 Candoa Workflow Sync
========================
📁 Project ID: 0e8b8d5e-cade-4bf5-9e9f-c9e55a5d5ca0
📋 Found 1 workflow file:
- /examples/welcome.ts
🔍 Processing: /examples/welcome.ts
📝 Syncing workflow: new-user-onboarding
✅ Created: new-user-onboarding
📝 Syncing workflow: frustration-escalation
✅ Created: frustration-escalation
📊 Sync Summary
===============
✅ Created: 2
🔄 Updated: 0
⏭️ Skipped: 1
❌ Errors: 0
🎉 Successfully synced 2 workflows!🎨 Integration with Candoa Systems
Contact Management
Works with your existing contact schema:
const contactWorkflow = workflow('contact-creation', {
description: 'Create comprehensive contact records',
})
.on('chat.started')
.use(
collectFirstName({ prompt: "What's your first name?" }),
collectEmail({ prompt: "What's your email?" }),
saveToContact({ field: 'firstName', value: '{{firstName}}' }),
saveToContact({ field: 'email', value: '{{email}}' }),
addTags({ tags: ['new-contact'], target: 'contact' }),
)Automated Handoff Integration
Enhances your existing handoff system:
const smartHandoff = workflow('smart-handoff', {
description: 'Intelligent handoff with data collection',
})
.on('sentiment.negative')
.use(
sendMessage({
message: 'Let me connect you with a human agent.',
}),
collectFirstName({ prompt: "What's your name?" }),
saveToContact({ field: 'firstName', value: '{{firstName}}' }),
handoffToHuman({
reason: 'Negative sentiment with contact data',
}),
)Execution Context
Build context from your database:
import { buildWorkflowContext } from '@candoa/workflows'
const context = buildWorkflowContext({
conversationId: conversation.id,
contactData: {
id: contact.id,
firstName: contact.name?.split(' ')[0],
email: contact.email,
phoneNumber: contact.phoneNumber,
},
messageHistory: messages.map((msg) => ({
role: msg.role,
content: msg.content,
createdAt: msg.createdAt,
})),
triggerData: {
sentiment: 'negative',
intent: 'billing_question',
},
})📖 Advanced Examples
Customer Onboarding Flow
export const customerOnboarding = workflow('customer-onboarding', {
description: 'Complete customer onboarding process',
})
.on('user.signup')
.use(
sendMessage({
message: "Welcome! Let's get you set up in just a few steps.",
}),
collectFirstName({ prompt: "First, what's your first name?" }),
collectEmail({ prompt: "What's your email address?" }),
saveToContact({ field: 'firstName', value: '{{firstName}}' }),
saveToContact({ field: 'email', value: '{{email}}' }),
sendMessage({ message: 'Perfect! Welcome aboard {{firstName}} 🎉' }),
addTags({ tags: ['onboarded'], target: 'contact' }),
createTask({
title: 'Send welcome materials to {{firstName}}',
assignee: 'onboarding-team',
}),
)Support Escalation
export const supportEscalation = workflow('support-escalation', {
description: 'Escalate negative sentiment to human agents',
})
.on('sentiment.negative')
.use(
sendMessage({
message:
'I sense you might be frustrated. Let me get you connected with a human agent.',
}),
handoffToHuman({
reason: 'Negative sentiment detected',
}),
createTask({
title: 'Urgent: Frustrated customer needs assistance',
}),
addTags({ tags: ['escalated', 'urgent'], target: 'conversation' }),
)Payment Recovery
export const paymentRecovery = workflow('payment-recovery', {
description: 'Handle failed payment with automated retry',
})
.on('payment.failed')
.use(
sendMessage({
message:
'It looks like there was an issue with your payment. Let me help you resolve this.',
}),
wait({ duration: 1, unit: 'hours' }),
sendEmail({
to: '{{email}}',
subject: 'Payment Issue - Action Required',
body: 'Hi {{firstName}}, we had trouble processing your payment. Please update your payment method.',
}),
createTask({
title: 'Follow up on failed payment for {{firstName}}',
description: 'Payment failed, email sent, needs follow-up',
}),
)📊 Analytics & Monitoring
Workflow Performance Tracking
import { extractWorkflowMetrics, updateWorkflowStats } from '@candoa/workflows'
// After workflow execution
const metrics = extractWorkflowMetrics(workflowDef, {
success: true,
executionTime: 1250, // milliseconds
actionsCompleted: 5,
})
// Update workflow stats in database
const updatedWorkflowData = updateWorkflowStats(
existingWorkflow.workflowData,
true, // was successful
)
await db
.update(workflows)
.set({ workflowData: updatedWorkflowData })
.where(eq(workflows.id, workflowId))🛠️ Best Practices
Organization
- Keep production workflows in
workflows/directory - Use
examples/for testing and documentation - Group related workflows in the same file
- Use descriptive workflow names and descriptions
Naming Conventions
- Use kebab-case for workflow names:
new-user-onboarding - Include purpose in name:
support-ticket-creation - Avoid conflicts with existing workflows
Development Workflow
- Develop workflows locally using the SDK
- Test with
--dry-runflag first - Sync to staging environment
- Validate in CRM interface
- Deploy to production
Version Control
- Commit workflow files to your repository
- Tag releases for workflow deployments
- Document changes in workflow descriptions
- Use branches for workflow development
🐛 Troubleshooting
Common Sync Issues
Project ID required:
# ❌ Missing project ID
pnpm run sync-workflows
# ✅ Provide project ID
pnpm run sync-workflows --project-id=your-uuidWorkflow already exists:
# ❌ Workflow exists, won't overwrite
pnpm run sync-workflows --project-id=your-uuid
# ✅ Use force flag to overwrite
pnpm run sync-workflows --project-id=your-uuid --forceValidation errors:
- Check workflow structure matches SDK format
- Ensure all required fields are present
- Verify action parameters are valid
- Review error details in sync output
Getting Help
- Review workflow examples in
examples/directory - Check sync output for detailed error messages
- Use
--dry-runto preview changes without applying - Validate workflows before syncing
🎨 IDE Support
This SDK is optimized for modern IDEs and GitHub Copilot:
- Auto-completion for trigger types, action parameters, and template variables
- Type checking for all parameters and return types
- IntelliSense documentation for all functions
- Error highlighting for invalid configurations
📄 License
MIT License - see LICENSE file for details
