diff --git a/README.md b/README.md
index b8fd70a..826a84e 100644
--- a/README.md
+++ b/README.md
@@ -380,7 +380,16 @@ await processEmails(payload)
await retryFailedEmails(payload)
```
-## PayloadCMS Task Integration
+## PayloadCMS Integration
+
+The plugin provides both tasks and workflows for email processing:
+
+### Tasks vs Workflows
+
+- **Tasks**: Simple job execution, good for background processing
+- **Workflows**: More advanced with UI, status tracking, and immediate processing options
+
+### Task Integration
The plugin provides a ready-to-use PayloadCMS task for queuing template emails:
@@ -455,6 +464,68 @@ The task can also be triggered from the Payload admin panel with a user-friendly
- ✅ **Error Handling**: Comprehensive error reporting
- ✅ **Queue Management**: Leverage Payload's job queue system
+### Workflow Integration
+
+The plugin also provides a workflow for sending emails with advanced features:
+
+```typescript
+import { sendEmailWorkflow } from '@xtr-dev/payload-mailing'
+
+export default buildConfig({
+ // ... your config
+ jobs: {
+ workflows: [
+ sendEmailWorkflow,
+ // ... your other workflows
+ ]
+ }
+})
+```
+
+### Workflow Features
+
+- **Immediate Processing**: Option to send emails immediately instead of queuing
+- **Admin UI**: Rich form interface with conditional fields
+- **Status Tracking**: Track workflow execution progress
+- **Template or Direct**: Support for both template-based and direct HTML emails
+
+### Using the Workflow
+
+```typescript
+// Queue a workflow with immediate processing
+await payload.workflows.queue({
+ workflow: 'send-email',
+ input: {
+ processImmediately: true, // Send immediately
+ templateSlug: 'welcome-email',
+ to: ['user@example.com'],
+ variables: { name: 'John Doe' }
+ }
+})
+
+// Queue normally (processed by background job)
+await payload.workflows.queue({
+ workflow: 'send-email',
+ input: {
+ processImmediately: false, // Default: false
+ subject: 'Direct Email',
+ html: '
Hello World!
',
+ to: ['user@example.com']
+ }
+})
+```
+
+### Workflow vs Task Comparison
+
+| Feature | Task | Workflow |
+|---------|------|----------|
+| Admin UI | Basic form | Rich form with conditions |
+| Immediate Processing | ❌ | ✅ (optional) |
+| Status Tracking | Basic | Detailed with progress |
+| Template Support | ✅ | ✅ |
+| Direct HTML Support | ✅ | ✅ |
+| Background Processing | ✅ | ✅ |
+
## Job Processing
The plugin automatically adds a unified email processing job to PayloadCMS:
diff --git a/package.json b/package.json
index cbc42a3..95580a9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@xtr-dev/payload-mailing",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Template-based email system with scheduling and job processing for PayloadCMS",
"type": "module",
"main": "dist/index.js",
diff --git a/src/index.ts b/src/index.ts
index bb6cd5b..15d5d5c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -15,6 +15,10 @@ export { default as Emails } from './collections/Emails.js'
export { mailingJobs, sendEmailJob } from './jobs/index.js'
export type { SendEmailTaskInput } from './jobs/sendEmailTask.js'
+// Workflows (includes the send email workflow)
+export { mailingWorkflows, sendEmailWorkflow } from './workflows/index.js'
+export type { SendEmailWorkflowInput } from './workflows/sendEmailWorkflow.js'
+
// Main email sending function
export { sendEmail, type SendEmailOptions } from './sendEmail.js'
export { default as sendEmailDefault } from './sendEmail.js'
diff --git a/src/plugin.ts b/src/plugin.ts
index 8cd05c1..efb056a 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -4,6 +4,7 @@ import { MailingService } from './services/MailingService.js'
import { createEmailTemplatesCollection } from './collections/EmailTemplates.js'
import Emails from './collections/Emails.js'
import { mailingJobs, scheduleEmailsJob } from './jobs/index.js'
+import { mailingWorkflows } from './workflows/index.js'
export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Config): Config => {
@@ -86,6 +87,10 @@ export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Con
...(config.jobs?.tasks || []),
...mailingJobs,
],
+ workflows: [
+ ...(config.jobs?.workflows || []),
+ ...mailingWorkflows,
+ ],
},
onInit: async (payload: any) => {
if (pluginConfig.initOrder === 'after' && config.onInit) {
diff --git a/src/services/MailingService.ts b/src/services/MailingService.ts
index d1860fb..c71d185 100644
--- a/src/services/MailingService.ts
+++ b/src/services/MailingService.ts
@@ -225,7 +225,7 @@ export class MailingService implements IMailingService {
}
}
- private async processEmailItem(emailId: string): Promise {
+ async processEmailItem(emailId: string): Promise {
try {
await this.payload.update({
collection: this.emailsCollection as any,
diff --git a/src/types/index.ts b/src/types/index.ts
index b545996..4c4a86c 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -111,6 +111,7 @@ export interface TemplateVariables {
export interface MailingService {
processEmails(): Promise
+ processEmailItem(emailId: string): Promise
retryFailedEmails(): Promise
renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }>
}
diff --git a/src/workflows/index.ts b/src/workflows/index.ts
new file mode 100644
index 0000000..6936e53
--- /dev/null
+++ b/src/workflows/index.ts
@@ -0,0 +1,11 @@
+import { sendEmailWorkflow } from './sendEmailWorkflow.js'
+
+/**
+ * All mailing-related workflows that get registered with Payload
+ */
+export const mailingWorkflows = [
+ sendEmailWorkflow,
+]
+
+// Re-export everything from individual workflow files
+export * from './sendEmailWorkflow.js'
\ No newline at end of file
diff --git a/src/workflows/sendEmailWorkflow.ts b/src/workflows/sendEmailWorkflow.ts
new file mode 100644
index 0000000..98697f0
--- /dev/null
+++ b/src/workflows/sendEmailWorkflow.ts
@@ -0,0 +1,295 @@
+import { sendEmail } from '../sendEmail.js'
+import { BaseEmailDocument } from '../types/index.js'
+
+export interface SendEmailWorkflowInput {
+ // Template mode fields
+ templateSlug?: string
+ variables?: Record
+
+ // Direct email mode fields
+ subject?: string
+ html?: string
+ text?: string
+
+ // Common fields
+ to: string | string[]
+ cc?: string | string[]
+ bcc?: string | string[]
+ from?: string
+ fromName?: string
+ replyTo?: string
+ scheduledAt?: string | Date // ISO date string or Date object
+ priority?: number
+
+ // Workflow-specific option
+ processImmediately?: boolean // If true, process the email immediately instead of waiting for the queue
+
+ // Allow any additional fields that users might have in their email collection
+ [key: string]: any
+}
+
+/**
+ * Transforms workflow input into sendEmail options by separating template and data fields
+ */
+function transformWorkflowInputToSendEmailOptions(workflowInput: SendEmailWorkflowInput) {
+ const sendEmailOptions: any = {
+ data: {}
+ }
+
+ // If using template mode, set template options
+ if (workflowInput.templateSlug) {
+ sendEmailOptions.template = {
+ slug: workflowInput.templateSlug,
+ variables: workflowInput.variables || {}
+ }
+ }
+
+ // Standard email fields that should be copied to data
+ const standardFields = ['to', 'cc', 'bcc', 'from', 'fromName', 'replyTo', 'subject', 'html', 'text', 'scheduledAt', 'priority']
+
+ // Fields that should not be copied to data
+ const excludedFields = ['templateSlug', 'variables', 'processImmediately']
+
+ // Copy standard fields to data
+ standardFields.forEach(field => {
+ if (workflowInput[field] !== undefined) {
+ sendEmailOptions.data[field] = workflowInput[field]
+ }
+ })
+
+ // Copy any additional custom fields
+ Object.keys(workflowInput).forEach(key => {
+ if (!excludedFields.includes(key) && !standardFields.includes(key)) {
+ sendEmailOptions.data[key] = workflowInput[key]
+ }
+ })
+
+ return sendEmailOptions
+}
+
+/**
+ * Workflow for sending emails with optional immediate processing
+ * Can be used through Payload's workflow system to send emails programmatically
+ */
+export const sendEmailWorkflow = {
+ slug: 'send-email',
+ label: 'Send Email',
+ inputSchema: [
+ {
+ name: 'processImmediately',
+ type: 'checkbox' as const,
+ label: 'Process Immediately',
+ defaultValue: false,
+ admin: {
+ description: 'Process and send the email immediately instead of waiting for the queue processor'
+ }
+ },
+ {
+ name: 'templateSlug',
+ type: 'text' as const,
+ label: 'Template Slug',
+ admin: {
+ description: 'Use a template (leave empty for direct email)',
+ condition: (data: any) => !data.subject && !data.html
+ }
+ },
+ {
+ name: 'variables',
+ type: 'json' as const,
+ label: 'Template Variables',
+ admin: {
+ description: 'JSON object with variables for template rendering',
+ condition: (data: any) => Boolean(data.templateSlug)
+ }
+ },
+ {
+ name: 'subject',
+ type: 'text' as const,
+ label: 'Subject',
+ admin: {
+ description: 'Email subject (required if not using template)',
+ condition: (data: any) => !data.templateSlug
+ }
+ },
+ {
+ name: 'html',
+ type: 'textarea' as const,
+ label: 'HTML Content',
+ admin: {
+ description: 'HTML email content (required if not using template)',
+ condition: (data: any) => !data.templateSlug
+ }
+ },
+ {
+ name: 'text',
+ type: 'textarea' as const,
+ label: 'Text Content',
+ admin: {
+ description: 'Plain text email content (optional)',
+ condition: (data: any) => !data.templateSlug
+ }
+ },
+ {
+ name: 'to',
+ type: 'text' as const,
+ required: true,
+ label: 'To (Email Recipients)',
+ admin: {
+ description: 'Comma-separated list of email addresses'
+ }
+ },
+ {
+ name: 'cc',
+ type: 'text' as const,
+ label: 'CC (Carbon Copy)',
+ admin: {
+ description: 'Optional comma-separated list of CC email addresses'
+ }
+ },
+ {
+ name: 'bcc',
+ type: 'text' as const,
+ label: 'BCC (Blind Carbon Copy)',
+ admin: {
+ description: 'Optional comma-separated list of BCC email addresses'
+ }
+ },
+ {
+ name: 'from',
+ type: 'text' as const,
+ label: 'From Email',
+ admin: {
+ description: 'Optional sender email address (uses default if not provided)'
+ }
+ },
+ {
+ name: 'fromName',
+ type: 'text' as const,
+ label: 'From Name',
+ admin: {
+ description: 'Optional sender display name (e.g., "John Doe")'
+ }
+ },
+ {
+ name: 'replyTo',
+ type: 'text' as const,
+ label: 'Reply To',
+ admin: {
+ description: 'Optional reply-to email address'
+ }
+ },
+ {
+ name: 'scheduledAt',
+ type: 'date' as const,
+ label: 'Schedule For',
+ admin: {
+ description: 'Optional date/time to schedule email for future delivery',
+ condition: (data: any) => !data.processImmediately
+ }
+ },
+ {
+ name: 'priority',
+ type: 'number' as const,
+ label: 'Priority',
+ min: 1,
+ max: 10,
+ defaultValue: 5,
+ admin: {
+ description: 'Email priority (1 = highest, 10 = lowest)'
+ }
+ }
+ ],
+ handler: async ({ job, req }: any) => {
+ const { input, id, taskStatus } = job
+ const { payload } = req
+
+ // Cast input to our expected type
+ const workflowInput = input as SendEmailWorkflowInput
+ const shouldProcessImmediately = workflowInput.processImmediately || false
+
+ try {
+ console.log(`📧 Workflow ${id}: Creating email...`)
+
+ // Transform workflow input into sendEmail options
+ const sendEmailOptions = transformWorkflowInputToSendEmailOptions(workflowInput)
+
+ // Create the email in the database
+ const email = await sendEmail(payload, sendEmailOptions)
+
+ console.log(`✅ Workflow ${id}: Email created with ID: ${email.id}`)
+
+ // Update task status with email ID
+ if (taskStatus) {
+ await taskStatus.update({
+ data: {
+ emailId: email.id,
+ status: 'created'
+ }
+ })
+ }
+
+ // If processImmediately is true, process the email now
+ if (shouldProcessImmediately) {
+ console.log(`⚡ Workflow ${id}: Processing email immediately...`)
+
+ // Get the mailing service from context
+ const mailingContext = payload.mailing
+ if (!mailingContext || !mailingContext.service) {
+ throw new Error('Mailing plugin not properly initialized')
+ }
+
+ // Process just this specific email
+ await mailingContext.service.processEmailItem(String(email.id))
+
+ console.log(`✅ Workflow ${id}: Email processed and sent immediately`)
+
+ // Update task status
+ if (taskStatus) {
+ await taskStatus.update({
+ data: {
+ emailId: email.id,
+ status: 'sent',
+ processedImmediately: true
+ }
+ })
+ }
+ } else {
+ // Update task status for queued email
+ if (taskStatus) {
+ await taskStatus.update({
+ data: {
+ emailId: email.id,
+ status: 'queued',
+ processedImmediately: false
+ }
+ })
+ }
+ }
+
+ } catch (error) {
+ console.error(`❌ Workflow ${id}: Failed to process email:`, error)
+
+ // Update task status with error
+ if (taskStatus) {
+ await taskStatus.update({
+ data: {
+ status: 'failed',
+ error: error instanceof Error ? error.message : String(error)
+ }
+ })
+ }
+
+ if (error instanceof Error) {
+ // Preserve original error and stack trace
+ const wrappedError = new Error(`Failed to process email: ${error.message}`)
+ wrappedError.stack = error.stack
+ wrappedError.cause = error
+ throw wrappedError
+ } else {
+ throw new Error(`Failed to process email: ${String(error)}`)
+ }
+ }
+ }
+}
+
+export default sendEmailWorkflow
\ No newline at end of file