16 Commits

Author SHA1 Message Date
25d42b4653 0.0.36 2025-09-09 13:52:11 +02:00
73c8c20c4b Improve logging system with environment variable control
- Change default log level to 'warn' for production
- Add PAYLOAD_AUTOMATION_LOG_LEVEL environment variable
- Remove all verbose config-phase logs
- Add documentation for log level control
2025-09-09 13:52:06 +02:00
e138176878 0.0.35 2025-09-09 12:47:47 +02:00
6245a71516 Remove all debugging and verbose logs for production
- Remove detailed trigger matching debug logs
- Remove verbose config initialization console output
- Clean up all 🚨 console.log debugging statements
- Change overly verbose logs to debug level
- Production-ready clean logging output
- Maintain essential error logging and workflow execution info
2025-09-09 12:46:37 +02:00
59a97e519e 0.0.34 2025-09-09 12:14:41 +02:00
b3d2877f0a Enhanced debugging and reduce verbose config logs
- Change trigger debugging from debug to info level for visibility
- Add trigger condition evaluation logging with doc status
- Reduce verbose plugin config logs that spam dev console
- Help customer diagnose trigger matching issues more effectively
2025-09-09 12:14:31 +02:00
c050ee835a 0.0.33 2025-09-09 11:58:50 +02:00
1f80028042 Add enhanced debugging for trigger matching
- Show detailed matching criteria for each trigger
- Display typeMatch, collectionMatch, operationMatch for debugging
- Help identify why triggers are not matching
- Assists in troubleshooting workflow execution issues
2025-09-09 11:58:45 +02:00
14d1ecf036 0.0.32 2025-09-09 11:38:50 +02:00
3749881d5f Fix workflow executor initialization timing issue
- Add lazy initialization when executor is not ready during hook execution
- Handles development hot-reloading scenarios where module registry resets
- Prevents 'Workflow executor not yet initialized' warnings
- Creates workflow executor on-demand when hooks fire before onInit
- Improved error handling and tracking for initialization failures

Resolves: Workflow executor timing issues in development environments
2025-09-09 11:38:40 +02:00
c46b58f43e 0.0.31 2025-09-09 11:11:40 +02:00
398a2d160e HOTFIX: Fix duplicate collectionSlug field error
- Multiple step types (create-document, read-document, etc.) were defining collectionSlug fields
- These created duplicate field names at the same level in the Workflow collection
- Fixed by prefixing step field names with step slug (__step_{stepSlug}_{fieldName})
- Added virtual field hooks to store/retrieve data using original field names
- Resolves DuplicateFieldName error preventing PayloadCMS initialization

Fixes: #duplicate-field-name-issue
Closes: User bug report for @xtr-dev/payload-automation@0.0.30
2025-09-09 11:11:31 +02:00
96b36a3caa 0.0.30 2025-09-09 10:30:38 +02:00
71ecca8253 Fix component import paths to use package imports
- Change component paths from relative to @xtr-dev/payload-automation/client#Component
- Use proper PayloadCMS plugin import syntax for components
- Regenerate import map with correct package-based imports
- Resolves 'Module not found' errors in dev project
2025-09-09 10:30:29 +02:00
8eedaba9ed 0.0.29 2025-09-09 10:13:04 +02:00
2bc01f30f8 Fix TypeScript and ESLint errors, resolve component imports
- Fix TypeScript types in trigger-helpers with proper interfaces
- Remove all ESLint no-explicit-any warnings with better typing
- Fix component import paths from @/components/* to relative paths
- Regenerate import map with correct component references
- All compilation and linting errors resolved
2025-09-09 10:13:00 +02:00
15 changed files with 463 additions and 155 deletions

View File

@@ -155,6 +155,34 @@ Use JSONPath to access workflow data:
- Node.js ^18.20.2 || >=20.9.0 - Node.js ^18.20.2 || >=20.9.0
- pnpm ^9 || ^10 - pnpm ^9 || ^10
## Environment Variables
Control plugin logging with these environment variables:
### `PAYLOAD_AUTOMATION_LOG_LEVEL`
Controls both configuration-time and runtime logging.
- **Values**: `silent`, `error`, `warn`, `info`, `debug`, `trace`
- **Default**: `warn`
- **Example**: `PAYLOAD_AUTOMATION_LOG_LEVEL=debug`
### `PAYLOAD_AUTOMATION_CONFIG_LOG_LEVEL` (optional)
Override log level specifically for configuration-time logs (plugin setup).
- **Values**: Same as above
- **Default**: Falls back to `PAYLOAD_AUTOMATION_LOG_LEVEL` or `warn`
- **Example**: `PAYLOAD_AUTOMATION_CONFIG_LOG_LEVEL=silent`
### Production Usage
For production, keep the default (`warn`) or use `error` or `silent`:
```bash
PAYLOAD_AUTOMATION_LOG_LEVEL=error npm start
```
### Development Usage
For debugging, use `debug` or `info`:
```bash
PAYLOAD_AUTOMATION_LOG_LEVEL=debug npm run dev
```
## Documentation ## Documentation
Full documentation coming soon. For now, explore the development environment in the repository for examples and patterns. Full documentation coming soon. For now, explore the development environment in the repository for examples and patterns.

View File

@@ -1,5 +1,9 @@
import { WorkflowExecutionStatus as WorkflowExecutionStatus_6f365a93b6cb4b34ad564b391e21db6f } from '@xtr-dev/payload-automation/client'
import { StatusCell as StatusCell_6f365a93b6cb4b34ad564b391e21db6f } from '@xtr-dev/payload-automation/client'
import { ErrorDisplay as ErrorDisplay_6f365a93b6cb4b34ad564b391e21db6f } from '@xtr-dev/payload-automation/client'
export const importMap = { export const importMap = {
"@xtr-dev/payload-automation/client#WorkflowExecutionStatus": WorkflowExecutionStatus_6f365a93b6cb4b34ad564b391e21db6f,
"@xtr-dev/payload-automation/client#StatusCell": StatusCell_6f365a93b6cb4b34ad564b391e21db6f,
"@xtr-dev/payload-automation/client#ErrorDisplay": ErrorDisplay_6f365a93b6cb4b34ad564b391e21db6f
} }

View File

@@ -92,7 +92,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>; 'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
}; };
db: { db: {
defaultIDType: number; defaultIDType: string;
}; };
globals: {}; globals: {};
globalsSelect: {}; globalsSelect: {};
@@ -136,7 +136,7 @@ export interface UserAuthOperations {
* via the `definition` "posts". * via the `definition` "posts".
*/ */
export interface Post { export interface Post {
id: number; id: string;
content?: string | null; content?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -146,7 +146,7 @@ export interface Post {
* via the `definition` "media". * via the `definition` "media".
*/ */
export interface Media { export interface Media {
id: number; id: string;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
url?: string | null; url?: string | null;
@@ -164,9 +164,9 @@ export interface Media {
* via the `definition` "auditLog". * via the `definition` "auditLog".
*/ */
export interface AuditLog { export interface AuditLog {
id: number; id: string;
post?: (number | null) | Post; post?: (string | null) | Post;
user?: (number | null) | User; user?: (string | null) | User;
message?: string | null; message?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -176,7 +176,7 @@ export interface AuditLog {
* via the `definition` "users". * via the `definition` "users".
*/ */
export interface User { export interface User {
id: number; id: string;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
email: string; email: string;
@@ -202,7 +202,7 @@ export interface User {
* via the `definition` "workflows". * via the `definition` "workflows".
*/ */
export interface Workflow { export interface Workflow {
id: number; id: string;
/** /**
* Human-readable name for the workflow * Human-readable name for the workflow
*/ */
@@ -214,36 +214,45 @@ export interface Workflow {
triggers?: triggers?:
| { | {
type?: ('collection-trigger' | 'webhook-trigger' | 'global-trigger' | 'cron-trigger') | null; type?: ('collection-trigger' | 'webhook-trigger' | 'global-trigger' | 'cron-trigger') | null;
parameters?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/** /**
* Collection that triggers the workflow * Collection that triggers the workflow
*/ */
collectionSlug?: ('posts' | 'media') | null; __builtin_collectionSlug?: ('posts' | 'media') | null;
/** /**
* Collection operation that triggers the workflow * Collection operation that triggers the workflow
*/ */
operation?: ('create' | 'delete' | 'read' | 'update') | null; __builtin_operation?: ('create' | 'delete' | 'read' | 'update') | null;
/** /**
* URL path for the webhook (e.g., "my-webhook"). Full URL will be /api/workflows/webhook/my-webhook * URL path for the webhook (e.g., "my-webhook"). Full URL will be /api/workflows-webhook/my-webhook
*/ */
webhookPath?: string | null; __builtin_webhookPath?: string | null;
/** /**
* Global that triggers the workflow * Global that triggers the workflow
*/ */
global?: string | null; __builtin_global?: string | null;
/** /**
* Global operation that triggers the workflow * Global operation that triggers the workflow
*/ */
globalOperation?: 'update' | null; __builtin_globalOperation?: 'update' | null;
/** /**
* Cron expression for scheduled execution (e.g., "0 0 * * *" for daily at midnight) * Cron expression for scheduled execution (e.g., "0 0 * * *" for daily at midnight)
*/ */
cronExpression?: string | null; __builtin_cronExpression?: string | null;
/** /**
* Timezone for cron execution (e.g., "America/New_York", "Europe/London"). Defaults to UTC. * Timezone for cron execution (e.g., "America/New_York", "Europe/London"). Defaults to UTC.
*/ */
timezone?: string | null; __builtin_timezone?: string | null;
/** /**
* JSONPath expression that must evaluate to true for this trigger to execute the workflow (e.g., "$.doc.status == 'published'") * JSONPath expression that must evaluate to true for this trigger to execute the workflow (e.g., "$.trigger.doc.status == 'published'")
*/ */
condition?: string | null; condition?: string | null;
id?: string | null; id?: string | null;
@@ -253,7 +262,18 @@ export interface Workflow {
| { | {
step?: ('http-request-step' | 'create-document') | null; step?: ('http-request-step' | 'create-document') | null;
name?: string | null; name?: string | null;
input?: /**
* The URL to make the HTTP request to
*/
url?: string | null;
/**
* HTTP method to use
*/
method?: ('GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH') | null;
/**
* HTTP headers as JSON object (e.g., {"Content-Type": "application/json"})
*/
headers?:
| { | {
[k: string]: unknown; [k: string]: unknown;
} }
@@ -262,6 +282,80 @@ export interface Workflow {
| number | number
| boolean | boolean
| null; | null;
/**
* Request body data. Use JSONPath to reference values (e.g., {"postId": "$.trigger.doc.id", "title": "$.trigger.doc.title"})
*/
body?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Request timeout in milliseconds (default: 30000)
*/
timeout?: number | null;
authentication?: {
/**
* Authentication method
*/
type?: ('none' | 'bearer' | 'basic' | 'apikey') | null;
/**
* Bearer token value
*/
token?: string | null;
/**
* Basic auth username
*/
username?: string | null;
/**
* Basic auth password
*/
password?: string | null;
/**
* API key header name (e.g., "X-API-Key")
*/
headerName?: string | null;
/**
* API key value
*/
headerValue?: string | null;
};
/**
* Number of retry attempts on failure (max: 5)
*/
retries?: number | null;
/**
* Delay between retries in milliseconds
*/
retryDelay?: number | null;
/**
* The collection slug to create a document in
*/
collectionSlug?: string | null;
/**
* The document data to create. Use JSONPath to reference trigger data (e.g., {"title": "$.trigger.doc.title", "author": "$.trigger.doc.author"})
*/
data?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Create as draft (if collection has drafts enabled)
*/
draft?: boolean | null;
/**
* Locale for the document (if localization is enabled)
*/
locale?: string | null;
/** /**
* Step names that must complete before this step can run * Step names that must complete before this step can run
*/ */
@@ -282,11 +376,11 @@ export interface Workflow {
* via the `definition` "workflow-runs". * via the `definition` "workflow-runs".
*/ */
export interface WorkflowRun { export interface WorkflowRun {
id: number; id: string;
/** /**
* Reference to the workflow that was executed * Reference to the workflow that was executed
*/ */
workflow: number | Workflow; workflow: string | Workflow;
/** /**
* Version of the workflow that was executed * Version of the workflow that was executed
*/ */
@@ -380,7 +474,7 @@ export interface WorkflowRun {
* via the `definition` "payload-jobs". * via the `definition` "payload-jobs".
*/ */
export interface PayloadJob { export interface PayloadJob {
id: number; id: string;
/** /**
* Input data provided to the job * Input data provided to the job
*/ */
@@ -472,40 +566,40 @@ export interface PayloadJob {
* via the `definition` "payload-locked-documents". * via the `definition` "payload-locked-documents".
*/ */
export interface PayloadLockedDocument { export interface PayloadLockedDocument {
id: number; id: string;
document?: document?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'media'; relationTo: 'media';
value: number | Media; value: string | Media;
} | null) } | null)
| ({ | ({
relationTo: 'auditLog'; relationTo: 'auditLog';
value: number | AuditLog; value: string | AuditLog;
} | null) } | null)
| ({ | ({
relationTo: 'workflows'; relationTo: 'workflows';
value: number | Workflow; value: string | Workflow;
} | null) } | null)
| ({ | ({
relationTo: 'workflow-runs'; relationTo: 'workflow-runs';
value: number | WorkflowRun; value: string | WorkflowRun;
} | null) } | null)
| ({ | ({
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
} | null) } | null)
| ({ | ({
relationTo: 'payload-jobs'; relationTo: 'payload-jobs';
value: number | PayloadJob; value: string | PayloadJob;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -515,10 +609,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences". * via the `definition` "payload-preferences".
*/ */
export interface PayloadPreference { export interface PayloadPreference {
id: number; id: string;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
}; };
key?: string | null; key?: string | null;
value?: value?:
@@ -538,7 +632,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations". * via the `definition` "payload-migrations".
*/ */
export interface PayloadMigration { export interface PayloadMigration {
id: number; id: string;
name?: string | null; name?: string | null;
batch?: number | null; batch?: number | null;
updatedAt: string; updatedAt: string;
@@ -592,13 +686,14 @@ export interface WorkflowsSelect<T extends boolean = true> {
| T | T
| { | {
type?: T; type?: T;
collectionSlug?: T; parameters?: T;
operation?: T; __builtin_collectionSlug?: T;
webhookPath?: T; __builtin_operation?: T;
global?: T; __builtin_webhookPath?: T;
globalOperation?: T; __builtin_global?: T;
cronExpression?: T; __builtin_globalOperation?: T;
timezone?: T; __builtin_cronExpression?: T;
__builtin_timezone?: T;
condition?: T; condition?: T;
id?: T; id?: T;
}; };
@@ -607,7 +702,27 @@ export interface WorkflowsSelect<T extends boolean = true> {
| { | {
step?: T; step?: T;
name?: T; name?: T;
input?: T; url?: T;
method?: T;
headers?: T;
body?: T;
timeout?: T;
authentication?:
| T
| {
type?: T;
token?: T;
username?: T;
password?: T;
headerName?: T;
headerValue?: T;
};
retries?: T;
retryDelay?: T;
collectionSlug?: T;
data?: T;
draft?: T;
locale?: T;
dependencies?: T; dependencies?: T;
condition?: T; condition?: T;
id?: T; id?: T;
@@ -736,10 +851,118 @@ export interface TaskWorkflowCronExecutor {
*/ */
export interface TaskHttpRequestStep { export interface TaskHttpRequestStep {
input: { input: {
url?: string | null; /**
* The URL to make the HTTP request to
*/
url: string;
/**
* HTTP method to use
*/
method?: ('GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH') | null;
/**
* HTTP headers as JSON object (e.g., {"Content-Type": "application/json"})
*/
headers?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Request body data. Use JSONPath to reference values (e.g., {"postId": "$.trigger.doc.id", "title": "$.trigger.doc.title"})
*/
body?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Request timeout in milliseconds (default: 30000)
*/
timeout?: number | null;
authentication?: {
/**
* Authentication method
*/
type?: ('none' | 'bearer' | 'basic' | 'apikey') | null;
/**
* Bearer token value
*/
token?: string | null;
/**
* Basic auth username
*/
username?: string | null;
/**
* Basic auth password
*/
password?: string | null;
/**
* API key header name (e.g., "X-API-Key")
*/
headerName?: string | null;
/**
* API key value
*/
headerValue?: string | null;
};
/**
* Number of retry attempts on failure (max: 5)
*/
retries?: number | null;
/**
* Delay between retries in milliseconds
*/
retryDelay?: number | null;
}; };
output: { output: {
response?: string | null; /**
* HTTP status code
*/
status?: number | null;
/**
* HTTP status text
*/
statusText?: string | null;
/**
* Response headers
*/
headers?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Response body
*/
body?: string | null;
/**
* Parsed response data (if JSON)
*/
data?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Request duration in milliseconds
*/
duration?: number | null;
}; };
} }
/** /**
@@ -753,7 +976,7 @@ export interface TaskCreateDocument {
*/ */
collectionSlug: string; collectionSlug: string;
/** /**
* The document data to create * The document data to create. Use JSONPath to reference trigger data (e.g., {"title": "$.trigger.doc.title", "author": "$.trigger.doc.author"})
*/ */
data: data:
| { | {

4
package-lock.json generated
View File

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

View File

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

View File

@@ -41,7 +41,7 @@ export const createWorkflowCollection: <T extends string>(options: WorkflowsPlug
type: 'ui', type: 'ui',
admin: { admin: {
components: { components: {
Field: '@/components/WorkflowExecutionStatus' Field: '@xtr-dev/payload-automation/client#WorkflowExecutionStatus'
}, },
condition: (data) => !!data?.id // Only show for existing workflows condition: (data) => !!data?.id // Only show for existing workflows
} }
@@ -319,17 +319,45 @@ export const createWorkflowCollection: <T extends string>(options: WorkflowsPlug
} }
] ]
}, },
...(steps || []).flatMap(step => (step.inputSchema || []).map(field => ({ ...(steps || []).flatMap(step => (step.inputSchema || []).map(field => {
...field, const originalName = (field as any).name;
admin: { const resultField: any = {
...(field.admin || {}), ...field,
condition: (...args) => args[1]?.step === step.slug && ( // Prefix field name with step slug to avoid conflicts
field.admin?.condition ? name: `__step_${step.slug}_${originalName}`,
field.admin.condition.call(this, ...args) : admin: {
true ...(field.admin || {}),
), condition: (...args: any[]) => args[1]?.step === step.slug && (
}, (field.admin as any)?.condition ?
} as Field))), (field.admin as any).condition.call(this, ...args) :
true
),
},
virtual: true,
};
// Add hooks to store/retrieve from the step's input data
resultField.hooks = {
...((field as any).hooks || {}),
afterRead: [
...(((field as any).hooks)?.afterRead || []),
({ siblingData }: any) => {
// Read from step input data using original field name
return siblingData?.[originalName] || (field as any).defaultValue;
}
],
beforeChange: [
...(((field as any).hooks)?.beforeChange || []),
({ siblingData, value }: any) => {
// Store in step data using original field name
siblingData[originalName] = value;
return undefined; // Don't store the prefixed field
}
]
};
return resultField as Field;
})),
{ {
name: 'dependencies', name: 'dependencies',
type: 'text', type: 'text',

View File

@@ -40,7 +40,7 @@ export const WorkflowRunsCollection: CollectionConfig = {
admin: { admin: {
description: 'Current execution status', description: 'Current execution status',
components: { components: {
Cell: '@/components/StatusCell' Cell: '@xtr-dev/payload-automation/client#StatusCell'
} }
}, },
defaultValue: 'pending', defaultValue: 'pending',
@@ -141,7 +141,7 @@ export const WorkflowRunsCollection: CollectionConfig = {
description: 'Error message if workflow execution failed', description: 'Error message if workflow execution failed',
condition: (_, siblingData) => siblingData?.status === 'failed', condition: (_, siblingData) => siblingData?.status === 'failed',
components: { components: {
Field: '@/components/ErrorDisplay' Field: '@xtr-dev/payload-automation/client#ErrorDisplay'
} }
}, },
}, },

View File

@@ -166,14 +166,15 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
{/* Technical Details Toggle */} {/* Technical Details Toggle */}
<div> <div>
<Button <div style={{ marginBottom: expanded ? '12px' : '0' }}>
onClick={() => setExpanded(!expanded)} <Button
size="small" onClick={() => setExpanded(!expanded)}
buttonStyle="secondary" size="small"
style={{ marginBottom: expanded ? '12px' : '0' }} buttonStyle="secondary"
> >
{expanded ? 'Hide' : 'Show'} Technical Details {expanded ? 'Hide' : 'Show'} Technical Details
</Button> </Button>
</div>
{expanded && ( {expanded && (
<div style={{ <div style={{

View File

@@ -990,12 +990,6 @@ export class WorkflowExecutor {
previousDoc: unknown, previousDoc: unknown,
req: PayloadRequest req: PayloadRequest
): Promise<void> { ): Promise<void> {
console.log('🚨 EXECUTOR: executeTriggeredWorkflows called!')
console.log('🚨 EXECUTOR: Collection =', collection)
console.log('🚨 EXECUTOR: Operation =', operation)
console.log('🚨 EXECUTOR: Doc ID =', (doc as any)?.id)
console.log('🚨 EXECUTOR: Has payload?', !!this.payload)
console.log('🚨 EXECUTOR: Has logger?', !!this.logger)
this.logger.info({ this.logger.info({
collection, collection,
@@ -1032,13 +1026,7 @@ export class WorkflowExecutor {
this.logger.debug({ this.logger.debug({
workflowId: workflow.id, workflowId: workflow.id,
workflowName: workflow.name, workflowName: workflow.name,
triggerCount: triggers?.length || 0, triggerCount: triggers?.length || 0
triggers: triggers?.map(t => ({
type: t.type,
collection: t.parameters?.collection,
collectionSlug: t.parameters?.collectionSlug,
operation: t.parameters?.operation
}))
}, 'Checking workflow triggers') }, 'Checking workflow triggers')
const matchingTriggers = triggers?.filter(trigger => const matchingTriggers = triggers?.filter(trigger =>
@@ -1087,12 +1075,9 @@ export class WorkflowExecutor {
collection, collection,
operation, operation,
condition: trigger.condition, condition: trigger.condition,
docId: (doc as any)?.id,
docFields: doc ? Object.keys(doc) : [],
previousDocId: (previousDoc as any)?.id,
workflowId: workflow.id, workflowId: workflow.id,
workflowName: workflow.name workflowName: workflow.name
}, 'Evaluating collection trigger condition') }, 'Evaluating trigger condition')
const conditionMet = this.evaluateCondition(trigger.condition, context) const conditionMet = this.evaluateCondition(trigger.condition, context)

View File

@@ -3,7 +3,7 @@
export { TriggerWorkflowButton } from '../components/TriggerWorkflowButton.js' export { TriggerWorkflowButton } from '../components/TriggerWorkflowButton.js'
export { StatusCell } from '../components/StatusCell.js' export { StatusCell } from '../components/StatusCell.js'
// export { ErrorDisplay } from '../components/ErrorDisplay.js' // Temporarily disabled export { ErrorDisplay } from '../components/ErrorDisplay.js'
export { WorkflowExecutionStatus } from '../components/WorkflowExecutionStatus.js' export { WorkflowExecutionStatus } from '../components/WorkflowExecutionStatus.js'
// Future client components can be added here: // Future client components can be added here:

View File

@@ -145,7 +145,6 @@ export const workflowsPlugin =
// CRITICAL: Modify existing collection configs BEFORE PayloadCMS processes them // CRITICAL: Modify existing collection configs BEFORE PayloadCMS processes them
// This is the ONLY time we can add hooks that will actually work // This is the ONLY time we can add hooks that will actually work
const logger = getConfigLogger() const logger = getConfigLogger()
logger.info('Attempting to modify collection configs before PayloadCMS initialization...')
if (config.collections && pluginOptions.collectionTriggers) { if (config.collections && pluginOptions.collectionTriggers) {
for (const [triggerSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) { for (const [triggerSlug, triggerConfig] of Object.entries(pluginOptions.collectionTriggers)) {
@@ -159,7 +158,6 @@ export const workflowsPlugin =
} }
const collection = config.collections[collectionIndex] const collection = config.collections[collectionIndex]
logger.info(`Found collection '${triggerSlug}' - modifying its hooks...`)
// Initialize hooks if needed // Initialize hooks if needed
if (!collection.hooks) { if (!collection.hooks) {
@@ -186,19 +184,40 @@ export const workflowsPlugin =
}, 'Collection automation hook triggered') }, 'Collection automation hook triggered')
if (!registry.isInitialized) { if (!registry.isInitialized) {
logger.warn('Workflow executor not yet initialized, skipping execution') logger.warn('Workflow executor not yet initialized, attempting lazy initialization')
return undefined
try {
// Try to create executor if we have a payload instance
if (args.req?.payload) {
logger.info('Creating workflow executor via lazy initialization')
const { WorkflowExecutor } = await import('../core/workflow-executor.js')
const executor = new WorkflowExecutor(args.req.payload, logger)
setWorkflowExecutor(executor, logger)
logger.info('Lazy initialization successful')
} else {
logger.error('Cannot lazy initialize - no payload instance available')
await createFailedWorkflowRun(args, 'Workflow executor not initialized and lazy initialization failed - no payload instance', logger)
return undefined
}
} catch (error) {
logger.error('Lazy initialization failed:', error)
const errorMessage = error instanceof Error ? error.message : String(error)
await createFailedWorkflowRun(args, `Workflow executor lazy initialization failed: ${errorMessage}`, logger)
return undefined
}
} }
if (!registry.executor) { // Re-check registry after potential lazy initialization
const updatedRegistry = getExecutorRegistry()
if (!updatedRegistry.executor) {
logger.error('Workflow executor is null despite being marked as initialized') logger.error('Workflow executor is null despite being marked as initialized')
// Create a failed workflow run to track this issue // Create a failed workflow run to track this issue
await createFailedWorkflowRun(args, 'Executor not available', logger) await createFailedWorkflowRun(args, 'Executor not available after initialization', logger)
return undefined return undefined
} }
logger.debug('Executing triggered workflows...') logger.debug('Executing triggered workflows...')
await registry.executor.executeTriggeredWorkflows( await updatedRegistry.executor.executeTriggeredWorkflows(
args.collection.slug, args.collection.slug,
args.operation, args.operation,
args.doc, args.doc,
@@ -245,7 +264,6 @@ export const workflowsPlugin =
// Add the hook to the collection config // Add the hook to the collection config
collection.hooks.afterChange.push(automationHook) collection.hooks.afterChange.push(automationHook)
logger.info(`Added automation hook to '${triggerSlug}' - hook count: ${collection.hooks.afterChange.length}`)
} }
} }
@@ -254,17 +272,13 @@ export const workflowsPlugin =
} }
const configLogger = getConfigLogger() const configLogger = getConfigLogger()
configLogger.info(`Configuring workflow plugin with ${Object.keys(pluginOptions.collectionTriggers || {}).length} collection triggers`)
// Generate cron tasks for workflows with cron triggers // Generate cron tasks for workflows with cron triggers
generateCronTasks(config) generateCronTasks(config)
for (const step of pluginOptions.steps) { for (const step of pluginOptions.steps) {
if (!config.jobs?.tasks?.find(task => task.slug === step.slug)) { if (!config.jobs?.tasks?.find(task => task.slug === step.slug)) {
configLogger.debug(`Registering task: ${step.slug}`)
config.jobs?.tasks?.push(step) config.jobs?.tasks?.push(step)
} else {
configLogger.debug(`Task ${step.slug} already registered, skipping`)
} }
} }
@@ -274,11 +288,9 @@ export const workflowsPlugin =
// Set up onInit to register collection hooks and initialize features // Set up onInit to register collection hooks and initialize features
const incomingOnInit = config.onInit const incomingOnInit = config.onInit
config.onInit = async (payload) => { config.onInit = async (payload) => {
configLogger.info(`onInit called - collections: ${Object.keys(payload.collections).length}`)
// Execute any existing onInit functions first // Execute any existing onInit functions first
if (incomingOnInit) { if (incomingOnInit) {
configLogger.debug('Executing existing onInit function')
await incomingOnInit(payload) await incomingOnInit(payload)
} }
@@ -290,10 +302,8 @@ export const workflowsPlugin =
logger.info(`Plugin configuration: ${Object.keys(pluginOptions.collectionTriggers || {}).length} collection triggers, ${pluginOptions.steps?.length || 0} steps`) logger.info(`Plugin configuration: ${Object.keys(pluginOptions.collectionTriggers || {}).length} collection triggers, ${pluginOptions.steps?.length || 0} steps`)
// Create workflow executor instance // Create workflow executor instance
console.log('🚨 CREATING WORKFLOW EXECUTOR INSTANCE') logger.debug('Creating workflow executor instance')
const executor = new WorkflowExecutor(payload, logger) const executor = new WorkflowExecutor(payload, logger)
console.log('🚨 EXECUTOR CREATED:', typeof executor)
console.log('🚨 EXECUTOR METHODS:', Object.getOwnPropertyNames(Object.getPrototypeOf(executor)))
// Register executor with proper dependency injection // Register executor with proper dependency injection
setWorkflowExecutor(executor, logger) setWorkflowExecutor(executor, logger)

View File

@@ -40,13 +40,6 @@ export function initCollectionHooks<T extends string>(pluginOptions: WorkflowsPl
collection.config.hooks.afterChange.push(async (change) => { collection.config.hooks.afterChange.push(async (change) => {
const operation = change.operation as 'create' | 'update' const operation = change.operation as 'create' | 'update'
// AGGRESSIVE LOGGING - this should ALWAYS appear
console.log('🚨 AUTOMATION PLUGIN HOOK CALLED! 🚨')
console.log('Collection:', change.collection.slug)
console.log('Operation:', operation)
console.log('Doc ID:', change.doc?.id)
console.log('Has executor?', !!executor)
console.log('Executor type:', typeof executor)
logger.info({ logger.info({
slug: change.collection.slug, slug: change.collection.slug,
@@ -55,10 +48,9 @@ export function initCollectionHooks<T extends string>(pluginOptions: WorkflowsPl
previousDocId: change.previousDoc?.id, previousDocId: change.previousDoc?.id,
hasExecutor: !!executor, hasExecutor: !!executor,
executorType: typeof executor executorType: typeof executor
}, 'AUTOMATION PLUGIN: Collection hook triggered') }, 'Collection automation hook triggered')
try { try {
console.log('🚨 About to call executeTriggeredWorkflows')
// Execute workflows for this trigger // Execute workflows for this trigger
await executor.executeTriggeredWorkflows( await executor.executeTriggeredWorkflows(
@@ -69,15 +61,13 @@ export function initCollectionHooks<T extends string>(pluginOptions: WorkflowsPl
change.req change.req
) )
console.log('🚨 executeTriggeredWorkflows completed without error')
logger.info({ logger.info({
slug: change.collection.slug, slug: change.collection.slug,
operation, operation,
docId: change.doc?.id docId: change.doc?.id
}, 'AUTOMATION PLUGIN: executeTriggeredWorkflows completed successfully') }, 'Workflow execution completed successfully')
} catch (error) { } catch (error) {
console.log('🚨 AUTOMATION PLUGIN ERROR:', error)
logger.error({ logger.error({
slug: change.collection.slug, slug: change.collection.slug,

View File

@@ -7,8 +7,6 @@ export function initWebhookEndpoint(config: Config, webhookPrefix = 'webhook'):
const logger = getConfigLogger() const logger = getConfigLogger()
// Ensure the prefix starts with a slash // Ensure the prefix starts with a slash
const normalizedPrefix = webhookPrefix.startsWith('/') ? webhookPrefix : `/${webhookPrefix}` const normalizedPrefix = webhookPrefix.startsWith('/') ? webhookPrefix : `/${webhookPrefix}`
logger.debug(`Adding webhook endpoint to config with prefix: ${normalizedPrefix}`)
logger.debug('Current config.endpoints length:', config.endpoints?.length || 0)
// Define webhook endpoint // Define webhook endpoint
const webhookEndpoint = { const webhookEndpoint = {
@@ -171,9 +169,5 @@ export function initWebhookEndpoint(config: Config, webhookPrefix = 'webhook'):
if (!existingEndpoint) { if (!existingEndpoint) {
// Combine existing endpoints with the webhook endpoint // Combine existing endpoints with the webhook endpoint
config.endpoints = [...(config.endpoints || []), webhookEndpoint] config.endpoints = [...(config.endpoints || []), webhookEndpoint]
logger.debug(`Webhook endpoint added at path: ${webhookEndpoint.path}`)
logger.debug('New config.endpoints length:', config.endpoints.length)
} else {
logger.debug(`Webhook endpoint already exists at path: ${webhookEndpoint.path}`)
} }
} }

View File

@@ -3,25 +3,40 @@ import type { Payload } from 'payload'
// Global logger instance - use Payload's logger type // Global logger instance - use Payload's logger type
let pluginLogger: null | Payload['logger'] = null let pluginLogger: null | Payload['logger'] = null
/**
* Get the configured log level from environment variables
* Supports: PAYLOAD_AUTOMATION_LOG_LEVEL for unified control
* Or separate: PAYLOAD_AUTOMATION_CONFIG_LOG_LEVEL and PAYLOAD_AUTOMATION_LOG_LEVEL
*/
function getConfigLogLevel(): string {
return process.env.PAYLOAD_AUTOMATION_CONFIG_LOG_LEVEL ||
process.env.PAYLOAD_AUTOMATION_LOG_LEVEL ||
'warn' // Default to warn level for production
}
/** /**
* Simple config-time logger for use during plugin configuration * Simple config-time logger for use during plugin configuration
* Uses console with plugin prefix since Payload logger isn't available yet * Uses console with plugin prefix since Payload logger isn't available yet
*/ */
const configLogger = { const configLogger = {
debug: <T>(message: string, ...args: T[]) => { debug: <T>(message: string, ...args: T[]) => {
if (!process.env.PAYLOAD_AUTOMATION_CONFIG_LOGGING) {return} const level = getConfigLogLevel()
console.log(`[payload-automation] ${message}`, ...args) if (level === 'silent' || (level !== 'debug' && level !== 'trace')) {return}
console.debug(`[payload-automation] ${message}`, ...args)
}, },
error: <T>(message: string, ...args: T[]) => { error: <T>(message: string, ...args: T[]) => {
if (!process.env.PAYLOAD_AUTOMATION_CONFIG_LOGGING) {return} const level = getConfigLogLevel()
if (level === 'silent') {return}
console.error(`[payload-automation] ${message}`, ...args) console.error(`[payload-automation] ${message}`, ...args)
}, },
info: <T>(message: string, ...args: T[]) => { info: <T>(message: string, ...args: T[]) => {
if (!process.env.PAYLOAD_AUTOMATION_CONFIG_LOGGING) {return} const level = getConfigLogLevel()
console.log(`[payload-automation] ${message}`, ...args) if (level === 'silent' || level === 'error' || level === 'warn') {return}
console.info(`[payload-automation] ${message}`, ...args)
}, },
warn: <T>(message: string, ...args: T[]) => { warn: <T>(message: string, ...args: T[]) => {
if (!process.env.PAYLOAD_AUTOMATION_CONFIG_LOGGING) {return} const level = getConfigLogLevel()
if (level === 'silent' || level === 'error') {return}
console.warn(`[payload-automation] ${message}`, ...args) console.warn(`[payload-automation] ${message}`, ...args)
} }
} }
@@ -39,8 +54,13 @@ export function getConfigLogger() {
*/ */
export function initializeLogger(payload: Payload): Payload['logger'] { export function initializeLogger(payload: Payload): Payload['logger'] {
// Create a child logger with plugin identification // Create a child logger with plugin identification
// Use PAYLOAD_AUTOMATION_LOG_LEVEL as the primary env var
const logLevel = process.env.PAYLOAD_AUTOMATION_LOG_LEVEL ||
process.env.PAYLOAD_AUTOMATION_LOGGING || // Legacy support
'warn' // Default to warn level for production
pluginLogger = payload.logger.child({ pluginLogger = payload.logger.child({
level: process.env.PAYLOAD_AUTOMATION_LOGGING || 'silent', level: logLevel,
plugin: '@xtr-dev/payload-automation' plugin: '@xtr-dev/payload-automation'
}) })
return pluginLogger return pluginLogger

View File

@@ -1,6 +1,22 @@
import type { Field } from 'payload' import type { Field } from 'payload'
import type { CustomTriggerConfig } from '../plugin/config-types.js' import type { CustomTriggerConfig } from '../plugin/config-types.js'
// Types for better type safety
interface FieldWithName {
name: string
[key: string]: unknown
}
interface HookContext {
siblingData: Record<string, unknown>
value?: unknown
}
interface ValidationContext {
siblingData: Record<string, unknown>
}
/** /**
* Creates a virtual field for a trigger parameter that stores its value in the parameters JSON field * Creates a virtual field for a trigger parameter that stores its value in the parameters JSON field
* *
@@ -25,7 +41,7 @@ import type { CustomTriggerConfig } from '../plugin/config-types.js'
* } * }
* ``` * ```
*/ */
export function createTriggerField(field: any, triggerSlug: string): Field { export function createTriggerField(field: FieldWithName, triggerSlug: string): Field {
const originalName = field.name const originalName = field.name
if (!originalName) { if (!originalName) {
throw new Error('Field must have a name property') throw new Error('Field must have a name property')
@@ -34,61 +50,70 @@ export function createTriggerField(field: any, triggerSlug: string): Field {
// Create a unique field name by prefixing with trigger slug // Create a unique field name by prefixing with trigger slug
const uniqueFieldName = `__trigger_${triggerSlug}_${originalName}` const uniqueFieldName = `__trigger_${triggerSlug}_${originalName}`
const resultField: any = { const resultField: Record<string, unknown> = {
...field, ...field,
name: uniqueFieldName,
virtual: true,
admin: { admin: {
...(field.admin || {}), ...(field.admin as Record<string, unknown> || {}),
condition: (data: any, siblingData: any) => { condition: (data: unknown, siblingData: Record<string, unknown>) => {
// Only show this field when the trigger type matches // Only show this field when the trigger type matches
const triggerMatches = siblingData?.type === triggerSlug const triggerMatches = siblingData?.type === triggerSlug
// If the original field had a condition, combine it with our trigger condition // If the original field had a condition, combine it with our trigger condition
if (field.admin?.condition) { const originalCondition = (field.admin as Record<string, unknown>)?.condition
return triggerMatches && field.admin.condition(data, siblingData) if (originalCondition && typeof originalCondition === 'function') {
return triggerMatches && (originalCondition as (data: unknown, siblingData: Record<string, unknown>) => boolean)(data, siblingData)
} }
return triggerMatches return triggerMatches
} }
}, },
hooks: { hooks: {
...(field.hooks || {}), ...(field.hooks as Record<string, unknown[]> || {}),
afterRead: [ afterRead: [
...(field.hooks?.afterRead || []), ...((field.hooks as Record<string, unknown[]>)?.afterRead || []),
({ siblingData }: any) => { ({ siblingData }: HookContext) => {
// Read the value from the parameters JSON field // Read the value from the parameters JSON field
return siblingData?.parameters?.[originalName] ?? field.defaultValue const parameters = siblingData?.parameters as Record<string, unknown>
return parameters?.[originalName] ?? (field as Record<string, unknown>).defaultValue
} }
], ],
beforeChange: [ beforeChange: [
...(field.hooks?.beforeChange || []), ...((field.hooks as Record<string, unknown[]>)?.beforeChange || []),
({ value, siblingData }: any) => { ({ siblingData, value }: HookContext) => {
// Store the value in the parameters JSON field // Store the value in the parameters JSON field
if (!siblingData.parameters) { if (!siblingData.parameters) {
siblingData.parameters = {} siblingData.parameters = {}
} }
siblingData.parameters[originalName] = value const parameters = siblingData.parameters as Record<string, unknown>
parameters[originalName] = value
return undefined // Virtual field, don't store directly return undefined // Virtual field, don't store directly
} }
] ]
} },
name: uniqueFieldName,
virtual: true,
} }
// Only add validate if the field supports it (data fields) // Only add validate if the field supports it (data fields)
if (field.validate || field.required) { const hasValidation = (field as Record<string, unknown>).validate || (field as Record<string, unknown>).required
resultField.validate = (value: any, args: any) => { if (hasValidation) {
const paramValue = value ?? args.siblingData?.parameters?.[originalName] resultField.validate = (value: unknown, args: ValidationContext) => {
const parameters = args.siblingData?.parameters as Record<string, unknown>
const paramValue = value ?? parameters?.[originalName]
// Check required validation // Check required validation
if (field.required && args.siblingData?.type === triggerSlug && !paramValue) { const isRequired = (field as Record<string, unknown>).required
const label = field.label || field.admin?.description || originalName if (isRequired && args.siblingData?.type === triggerSlug && !paramValue) {
const fieldLabel = (field as Record<string, unknown>).label as string
const adminDesc = ((field as Record<string, unknown>).admin as Record<string, unknown>)?.description as string
const label = fieldLabel || adminDesc || originalName
return `${label} is required for ${triggerSlug}` return `${label} is required for ${triggerSlug}`
} }
// Run original validation if present // Run original validation if present
if (field.validate) { const originalValidate = (field as Record<string, unknown>).validate
return field.validate(paramValue, args) if (originalValidate && typeof originalValidate === 'function') {
return (originalValidate as (value: unknown, args: ValidationContext) => boolean | string)(paramValue, args)
} }
return true return true
@@ -125,7 +150,7 @@ export function createTriggerField(field: any, triggerSlug: string): Field {
* ]) * ])
* ``` * ```
*/ */
export function createTrigger(slug: string, fields: Field[]): CustomTriggerConfig { export function createTrigger(slug: string, fields: FieldWithName[]): CustomTriggerConfig {
return { return {
slug, slug,
inputs: fields.map(field => createTriggerField(field, slug)) inputs: fields.map(field => createTriggerField(field, slug))