4 Commits

Author SHA1 Message Date
c24610b3d9 0.0.16 2025-09-01 20:32:03 +02:00
87893ac612 Fix critical hook initialization bug preventing workflow execution
- Remove problematic hooksInitialized flag that prevented proper hook registration in development mode
- Add comprehensive error logging with "AUTOMATION PLUGIN:" prefix for easier debugging
- Add try/catch blocks in hook execution to prevent silent failures
- Ensure hooks register properly on every PayloadCMS initialization

This fixes the issue where workflows would not execute even when properly configured.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-01 20:31:59 +02:00
a711fbdbea 0.0.15 2025-09-01 19:42:25 +02:00
4adc5cbdaa Fix workflow condition evaluation to support comparison operators
- Implemented proper parsing for conditions like '$.trigger.doc.content == "value"'
- Added support for comparison operators: ==, !=, >, <, >=, <=
- Fixed JSONPath condition evaluation that was treating entire expressions as JSONPath queries
- Added support for string literals, numbers, booleans in condition values
- Conditions now correctly resolve JSONPath expressions and perform comparisons

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-01 19:42:21 +02:00
12 changed files with 943 additions and 63 deletions

67
debug-customer-setup.js Normal file
View File

@@ -0,0 +1,67 @@
// Debug script to identify customer-side configuration issues
// Run this in your environment to pinpoint the problem
console.log('🔍 === CUSTOMER ENVIRONMENT DEBUGGING ===')
// This script needs to be run in your actual environment
// Copy this logic into your own debugging script
const debugChecklist = {
"Plugin Version": "Check package.json for @xtr-dev/payload-automation version",
"Plugin Configuration": "Verify automationPlugin() is called with correct collections array",
"Database Collections": "Confirm 'workflows' and 'workflow-runs' collections exist",
"Hook Registration": "Check if afterChange hooks are actually registered on orders collection",
"Workflow Status": "Verify workflow document has _status: 'published'",
"Workflow Structure": "Confirm triggers array and steps array are populated",
"Order Collection": "Verify orders collection exists and is configured in plugin",
"PayloadCMS Version": "Check if you're using compatible Payload version",
"Environment": "Development vs Production database differences"
}
console.log('\n📋 Debugging Checklist for Your Environment:')
Object.entries(debugChecklist).forEach(([check, description], i) => {
console.log(`${i + 1}. ${check}: ${description}`)
})
console.log('\n🔍 Specific Things to Check in YOUR Environment:')
console.log('\n1. Plugin Configuration (payload.config.ts):')
console.log(` automationPlugin({
collections: ['orders', 'users', 'products'], // <- Must include 'orders'
// ... other config
})`)
console.log('\n2. Database Query (run this in your environment):')
console.log(` const workflows = await payload.find({
collection: 'workflows',
depth: 2
})
console.log('Found workflows:', workflows.docs.length)
console.log('Workflow details:', JSON.stringify(workflows.docs, null, 2))`)
console.log('\n3. Hook Registration Check:')
console.log(` const orderCollection = payload.collections.orders
console.log('afterChange hooks:', orderCollection.config.hooks?.afterChange?.length)`)
console.log('\n4. Manual Hook Trigger Test:')
console.log(` // Manually call the executor method
const executor = // get executor instance somehow
await executor.executeTriggeredWorkflows('orders', 'update', updatedDoc, previousDoc, req)`)
console.log('\n5. Most Likely Issues:')
console.log(' - Plugin not configured with "orders" in collections array')
console.log(' - Workflow is in draft status (not published)')
console.log(' - Database connection issue (different DB in dev vs prod)')
console.log(' - PayloadCMS version compatibility issue')
console.log(' - Hook execution order (automation hook not running last)')
console.log('\n💡 Quick Test - Add this to your order update code:')
console.log(` console.log('🔍 DEBUG: About to update order')
const result = await payload.update({ ... })
console.log('🔍 DEBUG: Order updated, hooks should have fired')
// Check immediately after
const runs = await payload.find({ collection: 'workflow-runs' })
console.log('🔍 DEBUG: Workflow runs after update:', runs.docs.length)`)
process.exit(0)

210
debug-workflow-execution.js Normal file
View File

@@ -0,0 +1,210 @@
// Enhanced debugging script for workflow execution issues
const { getPayload } = require('payload')
const { JSONPath } = require('jsonpath-plus')
async function debugWorkflowExecution() {
const payload = await getPayload({
config: require('./dev/payload.config.ts').default
})
console.log('🔍 === WORKFLOW EXECUTION DEBUGGING ===')
// Step 1: Verify workflow exists and has correct structure
console.log('\n📋 Step 1: Finding workflows...')
const workflows = await payload.find({
collection: 'workflows',
depth: 2,
limit: 100
})
console.log(`Found ${workflows.docs.length} workflows:`)
for (const workflow of workflows.docs) {
console.log(`\n Workflow: "${workflow.name}" (ID: ${workflow.id})`)
console.log(` Enabled: ${workflow.enabled !== false}`)
console.log(` Triggers: ${JSON.stringify(workflow.triggers, null, 4)}`)
console.log(` Steps: ${JSON.stringify(workflow.steps, null, 4)}`)
}
// Step 2: Create test order and simulate the trigger context
console.log('\n📦 Step 2: Creating test order...')
const testOrder = await payload.create({
collection: 'orders',
data: {
orderName: 'Debug Test Order - ' + Date.now(),
status: 'Unpaid',
customerEmail: 'debug@example.com',
totalPrice: 1500,
items: [
{
name: 'Debug Item',
quantity: 1,
price: 1500
}
]
}
})
console.log(`Created test order: ${testOrder.id} with status: "${testOrder.status}"`)
// Step 3: Test JSONPath condition evaluation directly
console.log('\n🧪 Step 3: Testing JSONPath condition evaluation...')
// Simulate the execution context that would be created during hook execution
const simulatedContext = {
steps: {},
trigger: {
type: 'collection',
collection: 'orders',
doc: { ...testOrder, status: 'Paid' }, // Simulating the updated status
operation: 'update',
previousDoc: testOrder, // Original order with 'Unpaid' status
}
}
console.log('Simulated context:')
console.log(' - Trigger type:', simulatedContext.trigger.type)
console.log(' - Collection:', simulatedContext.trigger.collection)
console.log(' - Doc status:', simulatedContext.trigger.doc.status)
console.log(' - Previous doc status:', simulatedContext.trigger.previousDoc.status)
// Test the condition used in workflow
const condition = '$.doc.status == "Paid"'
console.log(`\nTesting condition: ${condition}`)
try {
// Test left side JSONPath resolution
const leftResult = JSONPath({
json: simulatedContext,
path: '$.trigger.doc.status',
wrap: false
})
console.log(` - Left side ($.trigger.doc.status): ${JSON.stringify(leftResult)} (type: ${typeof leftResult})`)
// Test the comparison manually
const comparisonMatch = condition.match(/^(.+?)\s*(==|!=|>|<|>=|<=)\s*(.+)$/)
if (comparisonMatch) {
const [, leftExpr, operator, rightExpr] = comparisonMatch
console.log(` - Left expression: "${leftExpr.trim()}"`)
console.log(` - Operator: "${operator}"`)
console.log(` - Right expression: "${rightExpr.trim()}"`)
// Parse right side (remove quotes if it's a string literal)
let rightValue = rightExpr.trim()
if (rightValue.startsWith('"') && rightValue.endsWith('"')) {
rightValue = rightValue.slice(1, -1)
}
console.log(` - Right value: "${rightValue}" (type: ${typeof rightValue})`)
const conditionResult = leftResult === rightValue
console.log(` - Condition result: ${conditionResult} (${leftResult} === ${rightValue})`)
}
} catch (error) {
console.error('❌ JSONPath evaluation failed:', error.message)
}
// Step 4: Test workflow trigger matching logic
console.log('\n🎯 Step 4: Testing trigger matching logic...')
for (const workflow of workflows.docs) {
console.log(`\nChecking workflow: "${workflow.name}"`)
const triggers = workflow.triggers
if (!triggers || !Array.isArray(triggers)) {
console.log(' ❌ No triggers found')
continue
}
for (const trigger of triggers) {
console.log(` Trigger details:`)
console.log(` - Type: ${trigger.type}`)
console.log(` - Collection: ${trigger.collection}`)
console.log(` - CollectionSlug: ${trigger.collectionSlug}`)
console.log(` - Operation: ${trigger.operation}`)
console.log(` - Condition: ${trigger.condition}`)
// Check basic matching criteria
const typeMatch = trigger.type === 'collection-trigger'
const collectionMatch = trigger.collection === 'orders' || trigger.collectionSlug === 'orders'
const operationMatch = trigger.operation === 'update'
console.log(` - Type match: ${typeMatch}`)
console.log(` - Collection match: ${collectionMatch}`)
console.log(` - Operation match: ${operationMatch}`)
if (typeMatch && collectionMatch && operationMatch) {
console.log(` ✅ Basic trigger criteria match!`)
if (trigger.condition) {
console.log(` Testing condition: ${trigger.condition}`)
// Note: We'd need to call the actual evaluateCondition method here
// but we're simulating the logic
} else {
console.log(` ✅ No condition required - this trigger should fire!`)
}
} else {
console.log(` ❌ Basic trigger criteria don't match`)
}
}
}
// Step 5: Update order and trace hook execution
console.log('\n🔄 Step 5: Updating order status to trigger workflow...')
console.log('Before update - checking existing workflow runs:')
const beforeRuns = await payload.find({
collection: 'workflow-runs'
})
console.log(` Existing workflow runs: ${beforeRuns.docs.length}`)
console.log('\nUpdating order status to "Paid"...')
const updatedOrder = await payload.update({
collection: 'orders',
id: testOrder.id,
data: {
status: 'Paid'
}
})
console.log(`Order updated successfully. New status: "${updatedOrder.status}"`)
// Wait a moment for async processing
await new Promise(resolve => setTimeout(resolve, 3000))
console.log('\nAfter update - checking for new workflow runs:')
const afterRuns = await payload.find({
collection: 'workflow-runs'
})
console.log(` Total workflow runs: ${afterRuns.docs.length}`)
console.log(` New runs created: ${afterRuns.docs.length - beforeRuns.docs.length}`)
if (afterRuns.docs.length > beforeRuns.docs.length) {
const newRuns = afterRuns.docs.slice(0, afterRuns.docs.length - beforeRuns.docs.length)
for (const run of newRuns) {
console.log(` - Run ID: ${run.id}`)
console.log(` - Workflow: ${run.workflow}`)
console.log(` - Status: ${run.status}`)
console.log(` - Context: ${JSON.stringify(run.context, null, 2)}`)
}
}
// Step 6: Check job queue
console.log('\n⚙ Step 6: Checking job queue...')
const jobs = await payload.find({
collection: 'payload-jobs',
sort: '-createdAt',
limit: 10
})
console.log(`Recent jobs in queue: ${jobs.docs.length}`)
for (const job of jobs.docs.slice(0, 5)) {
console.log(` - Job ${job.id}: ${job.taskSlug} (${job.processingError ? 'ERROR' : 'OK'})`)
}
console.log('\n✨ Debugging complete!')
process.exit(0)
}
debugWorkflowExecution().catch(console.error)

View File

@@ -0,0 +1,177 @@
// Enhanced debugging patch for workflow executor
// This temporarily patches the workflow executor to add comprehensive logging
import { getPayload } from 'payload'
async function patchAndTestWorkflow() {
const payload = await getPayload({
config: (await import('./dev/payload.config.ts')).default
})
console.log('🔧 === COMPREHENSIVE WORKFLOW DEBUGGING ===')
// Step 1: Check workflow collection structure and versioning
console.log('\n📋 Step 1: Analyzing workflow collection configuration...')
const workflowCollection = payload.collections.workflows
console.log('Workflow collection config:')
console.log(' - Slug:', workflowCollection.config.slug)
console.log(' - Versions enabled:', !!workflowCollection.config.versions)
console.log(' - Drafts enabled:', !!workflowCollection.config.versions?.drafts)
// Step 2: Test different query approaches for workflows
console.log('\n🔍 Step 2: Testing workflow queries...')
// Query 1: Default query (what the plugin currently uses)
console.log('Query 1: Default query (no status filter)')
try {
const workflows1 = await payload.find({
collection: 'workflows',
depth: 2,
limit: 100
})
console.log(` - Found: ${workflows1.docs.length} workflows`)
for (const wf of workflows1.docs) {
console.log(` - "${wf.name}" (ID: ${wf.id}) Status: ${wf._status || 'no-status'}`)
}
} catch (error) {
console.log(` - Error: ${error.message}`)
}
// Query 2: Only published workflows
console.log('\nQuery 2: Only published workflows')
try {
const workflows2 = await payload.find({
collection: 'workflows',
depth: 2,
limit: 100,
where: {
_status: {
equals: 'published'
}
}
})
console.log(` - Found: ${workflows2.docs.length} published workflows`)
for (const wf of workflows2.docs) {
console.log(` - "${wf.name}" (ID: ${wf.id}) Status: ${wf._status}`)
console.log(` Triggers: ${JSON.stringify(wf.triggers, null, 2)}`)
}
} catch (error) {
console.log(` - Error: ${error.message}`)
}
// Query 3: All workflows with explicit status
console.log('\nQuery 3: All workflows with status field')
try {
const workflows3 = await payload.find({
collection: 'workflows',
depth: 2,
limit: 100,
where: {
_status: {
exists: true
}
}
})
console.log(` - Found: ${workflows3.docs.length} workflows with status`)
for (const wf of workflows3.docs) {
console.log(` - "${wf.name}" Status: ${wf._status}`)
}
} catch (error) {
console.log(` - Error: ${error.message}`)
}
// Step 3: Create a test order and manually trigger the evaluation
console.log('\n📦 Step 3: Creating test order...')
const testOrder = await payload.create({
collection: 'orders',
data: {
orderName: 'Debug Comprehensive Test - ' + Date.now(),
status: 'Unpaid',
customerEmail: 'debug@example.com',
totalPrice: 1000,
items: [{
name: 'Debug Item',
quantity: 1,
price: 1000
}]
}
})
console.log(`Created order: ${testOrder.id} with status: ${testOrder.status}`)
// Step 4: Test the WorkflowExecutor.executeTriggeredWorkflows method directly
console.log('\n🎯 Step 4: Testing executeTriggeredWorkflows directly...')
// Access the workflow executor (this might require accessing internal plugin state)
// For now, let's simulate what should happen
console.log('Simulating executeTriggeredWorkflows call...')
console.log(' - Collection: orders')
console.log(' - Operation: update')
console.log(' - Doc: { ...order, status: "Paid" }')
console.log(' - PreviousDoc:', JSON.stringify(testOrder, null, 2))
// Step 5: Update the order and capture all logs
console.log('\n🔄 Step 5: Updating order with comprehensive logging...')
// First, let's check what hooks are actually registered
const orderCollection = payload.collections.orders
console.log('Order collection hooks:')
console.log(' - afterChange hooks:', orderCollection.config.hooks?.afterChange?.length || 0)
// Count current workflow runs before
const beforeRuns = await payload.find({ collection: 'workflow-runs' })
console.log(`Current workflow runs: ${beforeRuns.docs.length}`)
// Update the order
console.log('\nUpdating order status to "Paid"...')
const updatedOrder = await payload.update({
collection: 'orders',
id: testOrder.id,
data: { status: 'Paid' }
})
console.log(`Order updated: ${updatedOrder.status}`)
// Wait and check for workflow runs
console.log('Waiting 5 seconds for async processing...')
await new Promise(resolve => setTimeout(resolve, 5000))
const afterRuns = await payload.find({ collection: 'workflow-runs' })
console.log(`Workflow runs after: ${afterRuns.docs.length}`)
console.log(`New runs created: ${afterRuns.docs.length - beforeRuns.docs.length}`)
if (afterRuns.docs.length > beforeRuns.docs.length) {
console.log('✅ New workflow runs found!')
const newRuns = afterRuns.docs.slice(0, afterRuns.docs.length - beforeRuns.docs.length)
for (const run of newRuns) {
console.log(` - Run ${run.id}: ${run.status}`)
}
} else {
console.log('❌ No new workflow runs created')
// Additional debugging
console.log('\n🕵 Deep debugging - checking plugin state...')
// Check if the plugin is actually loaded
console.log('Available collections:', Object.keys(payload.collections))
// Check for recent jobs
const recentJobs = await payload.find({
collection: 'payload-jobs',
sort: '-createdAt',
limit: 5
})
console.log(`Recent jobs: ${recentJobs.docs.length}`)
for (const job of recentJobs.docs) {
console.log(` - ${job.taskSlug} (${job.processingError ? 'ERROR' : 'OK'})`)
}
}
console.log('\n✨ Comprehensive debugging complete!')
process.exit(0)
}
patchAndTestWorkflow().catch(console.error)

View File

@@ -103,7 +103,8 @@ const buildConfigWithMemoryDB = async () => {
plugins: [
workflowsPlugin<CollectionSlug>({
collectionTriggers: {
posts: true
posts: true,
media: true
},
steps: [
HttpRequestStepTask,

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@xtr-dev/payload-workflows",
"version": "0.0.14",
"version": "0.0.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@xtr-dev/payload-workflows",
"version": "0.0.14",
"version": "0.0.16",
"license": "MIT",
"dependencies": {
"jsonpath-plus": "^10.3.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@xtr-dev/payload-automation",
"version": "0.0.14",
"version": "0.0.16",
"description": "PayloadCMS Automation Plugin - Comprehensive workflow automation system with visual workflow building, execution tracking, and step types",
"license": "MIT",
"type": "module",

View File

@@ -480,7 +480,7 @@ export class WorkflowExecutor {
}
/**
* Evaluate a condition using JSONPath
* Evaluate a condition using JSONPath and comparison operators
*/
public evaluateCondition(condition: string, context: ExecutionContext): boolean {
this.logger.debug({
@@ -492,34 +492,94 @@ export class WorkflowExecutor {
}, 'Starting condition evaluation')
try {
const result = JSONPath({
json: context,
path: condition,
wrap: false
})
this.logger.debug({
condition,
result,
resultType: Array.isArray(result) ? 'array' : typeof result,
resultLength: Array.isArray(result) ? result.length : undefined
}, 'JSONPath evaluation result')
// Handle different result types
let finalResult: boolean
if (Array.isArray(result)) {
finalResult = result.length > 0 && Boolean(result[0])
// Check if this is a comparison expression
const comparisonMatch = condition.match(/^(.+?)\s*(==|!=|>|<|>=|<=)\s*(.+)$/)
if (comparisonMatch) {
const [, leftExpr, operator, rightExpr] = comparisonMatch
// Evaluate left side (should be JSONPath)
const leftValue = this.resolveJSONPathValue(leftExpr.trim(), context)
// Parse right side (could be string, number, boolean, or JSONPath)
const rightValue = this.parseConditionValue(rightExpr.trim(), context)
this.logger.debug({
condition,
leftExpr: leftExpr.trim(),
leftValue,
operator,
rightExpr: rightExpr.trim(),
rightValue,
leftType: typeof leftValue,
rightType: typeof rightValue
}, 'Evaluating comparison condition')
// Perform comparison
let result: boolean
switch (operator) {
case '==':
result = leftValue === rightValue
break
case '!=':
result = leftValue !== rightValue
break
case '>':
result = Number(leftValue) > Number(rightValue)
break
case '<':
result = Number(leftValue) < Number(rightValue)
break
case '>=':
result = Number(leftValue) >= Number(rightValue)
break
case '<=':
result = Number(leftValue) <= Number(rightValue)
break
default:
throw new Error(`Unknown comparison operator: ${operator}`)
}
this.logger.debug({
condition,
result,
leftValue,
rightValue,
operator
}, 'Comparison condition evaluation completed')
return result
} else {
finalResult = Boolean(result)
// Treat as simple JSONPath boolean evaluation
const result = JSONPath({
json: context,
path: condition,
wrap: false
})
this.logger.debug({
condition,
result,
resultType: Array.isArray(result) ? 'array' : typeof result,
resultLength: Array.isArray(result) ? result.length : undefined
}, 'JSONPath boolean evaluation result')
// Handle different result types
let finalResult: boolean
if (Array.isArray(result)) {
finalResult = result.length > 0 && Boolean(result[0])
} else {
finalResult = Boolean(result)
}
this.logger.debug({
condition,
finalResult,
originalResult: result
}, 'Boolean condition evaluation completed')
return finalResult
}
this.logger.debug({
condition,
finalResult,
originalResult: result
}, 'Condition evaluation completed')
return finalResult
} catch (error) {
this.logger.warn({
condition,
@@ -531,6 +591,49 @@ export class WorkflowExecutor {
return false
}
}
/**
* Resolve a JSONPath value from the context
*/
private resolveJSONPathValue(expr: string, context: ExecutionContext): any {
if (expr.startsWith('$')) {
const result = JSONPath({
json: context,
path: expr,
wrap: false
})
// Return first result if array, otherwise the result itself
return Array.isArray(result) && result.length > 0 ? result[0] : result
}
return expr
}
/**
* Parse a condition value (string literal, number, boolean, or JSONPath)
*/
private parseConditionValue(expr: string, context: ExecutionContext): any {
// Handle string literals
if ((expr.startsWith('"') && expr.endsWith('"')) || (expr.startsWith("'") && expr.endsWith("'"))) {
return expr.slice(1, -1) // Remove quotes
}
// Handle boolean literals
if (expr === 'true') return true
if (expr === 'false') return false
// Handle number literals
if (/^-?\d+(\.\d+)?$/.test(expr)) {
return Number(expr)
}
// Handle JSONPath expressions
if (expr.startsWith('$')) {
return this.resolveJSONPathValue(expr, context)
}
// Return as string if nothing else matches
return expr
}
/**
* Execute a workflow with the given context
@@ -555,19 +658,48 @@ export class WorkflowExecutor {
}
})
this.logger.info({
workflowId: workflow.id,
workflowName: workflow.name,
contextSummary: {
triggerType: context.trigger.type,
triggerCollection: context.trigger.collection,
triggerOperation: context.trigger.operation,
hasDoc: !!context.trigger.doc,
userEmail: context.trigger.req?.user?.email
}
}, 'About to create workflow run record')
// Create a workflow run record
const workflowRun = await this.payload.create({
collection: 'workflow-runs',
data: {
context: serializeContext(),
startedAt: new Date().toISOString(),
status: 'running',
triggeredBy: context.trigger.req?.user?.email || 'system',
workflow: workflow.id,
workflowVersion: 1 // Default version since generated type doesn't have _version field
},
req
})
let workflowRun;
try {
workflowRun = await this.payload.create({
collection: 'workflow-runs',
data: {
context: serializeContext(),
startedAt: new Date().toISOString(),
status: 'running',
triggeredBy: context.trigger.req?.user?.email || 'system',
workflow: workflow.id,
workflowVersion: 1 // Default version since generated type doesn't have _version field
},
req
})
this.logger.info({
workflowRunId: workflowRun.id,
workflowId: workflow.id,
workflowName: workflow.name
}, 'Workflow run record created successfully')
} catch (error) {
this.logger.error({
error: error instanceof Error ? error.message : 'Unknown error',
errorStack: error instanceof Error ? error.stack : undefined,
workflowId: workflow.id,
workflowName: workflow.name
}, 'Failed to create workflow run record')
throw error
}
try {
// Resolve execution order based on dependencies
@@ -711,6 +843,18 @@ export class WorkflowExecutor {
}, 'Matching triggers found')
for (const trigger of matchingTriggers) {
this.logger.info({
workflowId: workflow.id,
workflowName: workflow.name,
triggerDetails: {
type: trigger.type,
collection: trigger.collection,
collectionSlug: trigger.collectionSlug,
operation: trigger.operation,
hasCondition: !!trigger.condition
}
}, 'Processing matching trigger - about to execute workflow')
// Create execution context for condition evaluation
const context: ExecutionContext = {
steps: {},

View File

@@ -27,8 +27,6 @@ const applyCollectionsConfig = <T extends string>(pluginOptions: WorkflowsPlugin
)
}
// Track if hooks have been initialized to prevent double registration
let hooksInitialized = false
export const workflowsPlugin =
<TSlug extends string>(pluginOptions: WorkflowsPluginConfig<TSlug>) =>
@@ -65,13 +63,7 @@ export const workflowsPlugin =
// Set up onInit to register collection hooks and initialize features
const incomingOnInit = config.onInit
config.onInit = async (payload) => {
configLogger.info(`onInit called - hooks already initialized: ${hooksInitialized}, collections: ${Object.keys(payload.collections).length}`)
// Prevent double initialization in dev mode
if (hooksInitialized) {
configLogger.warn('Hooks already initialized, skipping to prevent duplicate registration')
return
}
configLogger.info(`onInit called - collections: ${Object.keys(payload.collections).length}`)
// Execute any existing onInit functions first
if (incomingOnInit) {
@@ -107,7 +99,6 @@ export const workflowsPlugin =
await registerCronJobs(payload, logger)
logger.info('Plugin initialized successfully - all hooks registered')
hooksInitialized = true
}
return config

View File

@@ -39,19 +39,37 @@ export function initCollectionHooks<T extends string>(pluginOptions: WorkflowsPl
collection.config.hooks.afterChange = collection.config.hooks.afterChange || []
collection.config.hooks.afterChange.push(async (change) => {
const operation = change.operation as 'create' | 'update'
logger.debug({
logger.info({
slug: change.collection.slug,
operation,
}, 'Collection hook triggered')
docId: change.doc?.id,
previousDocId: change.previousDoc?.id,
}, 'AUTOMATION PLUGIN: Collection hook triggered')
// Execute workflows for this trigger
await executor.executeTriggeredWorkflows(
change.collection.slug,
operation,
change.doc,
change.previousDoc,
change.req
)
try {
// Execute workflows for this trigger
await executor.executeTriggeredWorkflows(
change.collection.slug,
operation,
change.doc,
change.previousDoc,
change.req
)
logger.info({
slug: change.collection.slug,
operation,
docId: change.doc?.id
}, 'AUTOMATION PLUGIN: executeTriggeredWorkflows completed successfully')
} catch (error) {
logger.error({
slug: change.collection.slug,
operation,
docId: change.doc?.id,
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined
}, 'AUTOMATION PLUGIN: executeTriggeredWorkflows failed')
// Don't re-throw to avoid breaking other hooks
}
})
}

115
test-jsonpath-condition.js Normal file
View File

@@ -0,0 +1,115 @@
// Isolated JSONPath condition testing
import { JSONPath } from 'jsonpath-plus'
function testJSONPathCondition() {
console.log('🧪 Testing JSONPath condition evaluation in isolation')
// Simulate the exact context structure from workflow execution
const testContext = {
steps: {},
trigger: {
type: 'collection',
collection: 'orders',
doc: {
id: '12345',
orderName: 'Test Order',
status: 'Paid', // This is the updated status
customerEmail: 'test@example.com',
totalPrice: 2500
},
operation: 'update',
previousDoc: {
id: '12345',
orderName: 'Test Order',
status: 'Unpaid', // This was the previous status
customerEmail: 'test@example.com',
totalPrice: 2500
}
}
}
console.log('Test context:')
console.log(' - trigger.doc.status:', testContext.trigger.doc.status)
console.log(' - trigger.previousDoc.status:', testContext.trigger.previousDoc.status)
// Test different JSONPath expressions
const testCases = [
'$.trigger.doc.status',
'$.doc.status', // This is what your condition uses but might be wrong!
'$.trigger.doc.status == "Paid"',
'$.trigger.doc.status == "Unpaid"'
]
console.log('\n📋 Testing JSONPath expressions:')
for (const expression of testCases) {
try {
const result = JSONPath({
json: testContext,
path: expression,
wrap: false
})
console.log(`${expression} => ${JSON.stringify(result)} (${typeof result})`)
} catch (error) {
console.log(`${expression} => ERROR: ${error.message}`)
}
}
// Test comparison logic manually
console.log('\n🔍 Testing comparison logic:')
const condition = '$.doc.status == "Paid"' // Your original condition
const correctCondition = '$.trigger.doc.status == "Paid"' // Likely correct path
console.log(`\nTesting: ${condition}`)
try {
const leftResult = JSONPath({
json: testContext,
path: '$.doc.status',
wrap: false
})
console.log(` - Left side result: ${JSON.stringify(leftResult)}`)
console.log(` - Is undefined/null? ${leftResult === undefined || leftResult === null}`)
console.log(` - Comparison result: ${leftResult === 'Paid'}`)
} catch (error) {
console.log(` - Error: ${error.message}`)
}
console.log(`\nTesting: ${correctCondition}`)
try {
const leftResult = JSONPath({
json: testContext,
path: '$.trigger.doc.status',
wrap: false
})
console.log(` - Left side result: ${JSON.stringify(leftResult)}`)
console.log(` - Comparison result: ${leftResult === 'Paid'}`)
} catch (error) {
console.log(` - Error: ${error.message}`)
}
// Test regex parsing
console.log('\n📝 Testing regex parsing:')
const testConditions = [
'$.trigger.doc.status == "Paid"',
'$.doc.status == "Paid"',
'$.trigger.doc.status=="Paid"', // No spaces
"$.trigger.doc.status == 'Paid'" // Single quotes
]
for (const cond of testConditions) {
const comparisonMatch = cond.match(/^(.+?)\s*(==|!=|>|<|>=|<=)\s*(.+)$/)
if (comparisonMatch) {
const [, leftExpr, operator, rightExpr] = comparisonMatch
console.log(`${cond}`)
console.log(` - Left: "${leftExpr.trim()}"`)
console.log(` - Operator: "${operator}"`)
console.log(` - Right: "${rightExpr.trim()}"`)
} else {
console.log(`${cond} - No regex match`)
}
}
}
testJSONPathCondition()

View File

@@ -0,0 +1,44 @@
// Test script to verify published workflow filtering
console.log('🔍 Testing published workflow filtering...')
// This will be run from the dev environment
// Start the dev server first: pnpm dev
// Then in another terminal: node test-published-workflows.js
const testData = {
// Simulate what the workflow executor should find
allWorkflows: [
{
id: 1,
name: 'Draft Workflow',
_status: 'draft',
triggers: [{ type: 'collection-trigger', collectionSlug: 'orders', operation: 'update' }]
},
{
id: 2,
name: 'Published Workflow',
_status: 'published',
triggers: [{ type: 'collection-trigger', collectionSlug: 'orders', operation: 'update' }]
}
]
}
// Test filtering logic
const publishedOnly = testData.allWorkflows.filter(wf => wf._status === 'published')
console.log('All workflows:', testData.allWorkflows.length)
console.log('Published workflows:', publishedOnly.length)
console.log('Published workflow names:', publishedOnly.map(wf => wf.name))
console.log('\n✅ The published status filter should work!')
console.log('💡 Make sure your workflow has _status: "published" in the database')
// Instructions for manual verification
console.log('\n📋 Manual verification steps:')
console.log('1. Start dev server: pnpm dev')
console.log('2. Go to http://localhost:3000/admin/collections/workflows')
console.log('3. Find your workflow and ensure it shows as "Published" (not "Draft")')
console.log('4. If it shows as "Draft", click it and click "Publish"')
console.log('5. Then test your order status change again')
process.exit(0)

113
test-workflow-creation.js Normal file
View File

@@ -0,0 +1,113 @@
// Test script to create workflow with correct v0.0.15 schema structure
const { getPayload } = require('payload')
async function testWorkflowCreation() {
const payload = await getPayload({
config: require('./dev/payload.config.ts').default
})
console.log('🚀 Creating workflow with v0.0.15 schema...')
try {
const workflow = await payload.create({
collection: 'workflows',
data: {
name: 'Test Order Status Workflow v0.0.15',
description: 'Test workflow that triggers when order status changes to Paid',
enabled: true,
triggers: [
{
type: 'collection-trigger',
collectionSlug: 'orders',
operation: 'update',
// v0.0.15 uses 'condition' (singular) with JSONPath expressions
// instead of 'conditions' array
condition: '$.doc.status == "Paid"'
}
],
steps: [
{
// v0.0.15 uses 'step' field instead of 'type'
step: 'uppercaseText',
name: 'Test Uppercase Step',
// v0.0.15 uses 'input' (singular) instead of 'inputs'
input: {
inputText: 'Order {{$.trigger.doc.orderName}} has been paid!'
}
}
]
}
})
console.log('✅ Workflow created successfully!')
console.log('📋 Workflow details:')
console.log(' - ID:', workflow.id)
console.log(' - Name:', workflow.name)
console.log(' - Triggers:', JSON.stringify(workflow.triggers, null, 2))
console.log(' - Steps:', JSON.stringify(workflow.steps, null, 2))
// Now test with an order update
console.log('\n🔄 Testing order status change...')
// First create a test order
const order = await payload.create({
collection: 'orders',
data: {
orderName: 'Test Order - ' + Date.now(),
status: 'Unpaid',
customerEmail: 'test@example.com',
totalPrice: 2500,
items: [
{
name: 'Test Item',
quantity: 1,
price: 2500
}
]
}
})
console.log('📦 Test order created:', order.id)
// Update order status to trigger workflow
const updatedOrder = await payload.update({
collection: 'orders',
id: order.id,
data: {
status: 'Paid'
}
})
console.log('💰 Order status updated to:', updatedOrder.status)
// Wait a moment for async workflow execution
await new Promise(resolve => setTimeout(resolve, 2000))
// Check for workflow runs
const workflowRuns = await payload.find({
collection: 'workflow-runs',
where: {
workflow: {
equals: workflow.id
}
}
})
console.log(`\n📊 Workflow runs found: ${workflowRuns.docs.length}`)
if (workflowRuns.docs.length > 0) {
const run = workflowRuns.docs[0]
console.log(' - Run ID:', run.id)
console.log(' - Status:', run.status)
console.log(' - Context:', JSON.stringify(run.context, null, 2))
}
} catch (error) {
console.error('❌ Error:', error.message)
console.error('Stack:', error.stack)
}
process.exit(0)
}
testWorkflowCreation()