Skip to content

Background Actions

Cronflow actions are designed to run in the background, allowing you to perform side effects without blocking the main workflow execution. Actions are perfect for logging, notifications, cleanup operations, and other non-critical tasks.

Overview

Actions differ from steps in several key ways:

  • Output Ignored: Action outputs are not stored or accessible by subsequent steps
  • Background Execution: Actions run asynchronously without blocking the workflow
  • Side Effects: Perfect for operations where you don't need the result
  • Non-Critical: Actions don't affect the main workflow logic

Basic Actions

Simple Action

typescript
const workflow = cronflow.define({
  id: 'action-workflow',
  name: 'Action Workflow',
});

workflow
  .step('create-user', async ctx => {
    return await createUser(ctx.payload);
  })
  .action('send-welcome-email', async ctx => {
    // This runs in the background
    await sendEmail(ctx.last.email, 'Welcome to our platform!');
    // Return value is ignored
  })
  .step('log-creation', async ctx => {
    // This step can access the user data from the previous step
    console.log('User created:', ctx.last);
    return { logged: true };
  });

Action with Error Handling

typescript
workflow
  .step('process-order', async ctx => {
    return await processOrder(ctx.payload);
  })
  .action('send-notification', async ctx => {
    try {
      await sendNotification(ctx.last.customerId, 'Order processed');
    } catch (error) {
      // Log error but don't fail the workflow
      console.error('Notification failed:', error);
    }
  })
  .action('update-analytics', async ctx => {
    try {
      await analytics.track('order_processed', {
        orderId: ctx.last.id,
        amount: ctx.last.amount,
      });
    } catch (error) {
      console.error('Analytics update failed:', error);
    }
  });

Action Use Cases

1. Logging and Monitoring

typescript
const loggingWorkflow = cronflow.define({
  id: 'logging-workflow',
  name: 'Logging Workflow',
});

loggingWorkflow
  .action('log-start', async ctx => {
    console.log('Workflow started:', {
      timestamp: new Date().toISOString(),
      payload: ctx.payload,
    });
  })
  .step('process-data', async ctx => {
    return await processData(ctx.payload);
  })
  .action('log-success', async ctx => {
    console.log('Workflow completed successfully:', {
      timestamp: new Date().toISOString(),
      result: ctx.last,
    });
  })
  .action('send-metrics', async ctx => {
    await metrics.increment('workflow.success');
    await metrics.timing('workflow.duration', Date.now() - ctx.run.startTime);
  });

2. Notifications

typescript
const notificationWorkflow = cronflow.define({
  id: 'notification-workflow',
  name: 'Notification Workflow',
});

notificationWorkflow
  .step('process-payment', async ctx => {
    return await processPayment(ctx.payload);
  })
  .action('send-payment-confirmation', async ctx => {
    await emailService.send({
      to: ctx.last.customerEmail,
      subject: 'Payment Confirmed',
      template: 'payment-confirmation',
      data: ctx.last,
    });
  })
  .action('send-sms-notification', async ctx => {
    await smsService.send({
      to: ctx.last.customerPhone,
      message: `Payment of $${ctx.last.amount} confirmed`,
    });
  })
  .action('update-customer-dashboard', async ctx => {
    await dashboardService.updatePaymentStatus(
      ctx.last.customerId,
      'confirmed'
    );
  });

3. Cleanup Operations

typescript
const cleanupWorkflow = cronflow.define({
  id: 'cleanup-workflow',
  name: 'Cleanup Workflow',
});

cleanupWorkflow
  .step('process-file', async ctx => {
    return await processFile(ctx.payload.filePath);
  })
  .action('cleanup-temp-files', async ctx => {
    // Clean up temporary files in background
    await cleanupTempFiles(ctx.payload.filePath);
  })
  .action('archive-processed-file', async ctx => {
    // Archive the processed file
    await archiveFile(ctx.payload.filePath, 'processed');
  })
  .action('update-storage-metrics', async ctx => {
    // Update storage usage metrics
    await storageService.updateMetrics();
  });

4. Database Operations

typescript
const databaseWorkflow = cronflow.define({
  id: 'database-workflow',
  name: 'Database Workflow',
});

databaseWorkflow
  .step('create-record', async ctx => {
    return await db.records.create({
      data: ctx.payload,
    });
  })
  .action('update-cache', async ctx => {
    // Update cache in background
    await cache.set(`record-${ctx.last.id}`, ctx.last);
  })
  .action('update-search-index', async ctx => {
    // Update search index
    await searchIndex.index(ctx.last);
  })
  .action('send-webhook', async ctx => {
    // Send webhook notification
    await webhookService.notify('record.created', ctx.last);
  });

Advanced Action Patterns

1. Conditional Actions

typescript
const conditionalActionWorkflow = cronflow.define({
  id: 'conditional-action-workflow',
  name: 'Conditional Action Workflow',
});

conditionalActionWorkflow
  .step('process-order', async ctx => {
    return await processOrder(ctx.payload);
  })
  .if('is-high-value', ctx => ctx.last.amount > 1000)
  .action('send-vip-notification', async ctx => {
    await sendVipNotification(ctx.last.customerId);
  })
  .else()
  .action('send-standard-notification', async ctx => {
    await sendStandardNotification(ctx.last.customerId);
  })
  .endIf()
  .action('log-order-processed', async ctx => {
    await logOrderProcessed(ctx.last);
  });

2. Parallel Actions

typescript
const parallelActionWorkflow = cronflow.define({
  id: 'parallel-action-workflow',
  name: 'Parallel Action Workflow',
});

parallelActionWorkflow
  .step('process-data', async ctx => {
    return await processData(ctx.payload);
  })
  .parallel([
    async ctx => {
      await updateCache(ctx.last);
    },
    async ctx => {
      await updateAnalytics(ctx.last);
    },
    async ctx => {
      await sendNotification(ctx.last);
    },
  ])
  .action('final-cleanup', async ctx => {
    await finalCleanup();
  });

3. Action with Retry Logic

typescript
const retryActionWorkflow = cronflow.define({
  id: 'retry-action-workflow',
  name: 'Retry Action Workflow',
});

retryActionWorkflow
  .step('process-data', async ctx => {
    return await processData(ctx.payload);
  })
  .action('send-notification', async ctx => {
    let attempts = 0;
    const maxAttempts = 3;

    while (attempts < maxAttempts) {
      try {
        await sendNotification(ctx.last);
        break;
      } catch (error) {
        attempts++;
        if (attempts >= maxAttempts) {
          console.error('Notification failed after all attempts:', error);
        } else {
          await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
        }
      }
    }
  });

4. Action with Circuit Breaker

typescript
const circuitBreakerActionWorkflow = cronflow.define({
  id: 'circuit-breaker-action-workflow',
  name: 'Circuit Breaker Action Workflow',
});

circuitBreakerActionWorkflow
  .step('process-data', async ctx => {
    return await processData(ctx.payload);
  })
  .action('external-api-call', async ctx => {
    const failureCount = await ctx.state.get('api-failures', 0);

    if (failureCount >= 5) {
      console.log('Circuit breaker open, skipping API call');
      return;
    }

    try {
      await externalApi.call(ctx.last);
      await ctx.state.set('api-failures', 0);
    } catch (error) {
      const currentFailures = await ctx.state.get('api-failures', 0);
      await ctx.state.set('api-failures', currentFailures + 1);
      console.error('API call failed:', error);
    }
  });

Action Configuration

Action with Options

typescript
const configuredActionWorkflow = cronflow.define({
  id: 'configured-action-workflow',
  name: 'Configured Action Workflow',
});

configuredActionWorkflow
  .step('process-data', async ctx => {
    return await processData(ctx.payload);
  })
  .action(
    'send-notification',
    async ctx => {
      await sendNotification(ctx.last);
    },
    {
      timeout: '30s', // Action timeout
      retry: {
        attempts: 3,
        backoff: {
          strategy: 'exponential',
          delay: '1s',
        },
      },
    }
  );

Action with Custom Error Handling

typescript
workflow
  .step('process-data', async ctx => {
    return await processData(ctx.payload);
  })
  .action('risky-action', async ctx => {
    await riskyOperation(ctx.last);
  })
  .onError(ctx => {
    // Handle action errors
    console.error('Action failed:', ctx.error);
    return { actionError: ctx.error.message };
  });

Best Practices

1. Use Actions for Side Effects

typescript
// Good: Use actions for side effects
workflow
  .step('create-user', async ctx => {
    return await createUser(ctx.payload);
  })
  .action('send-welcome-email', async ctx => {
    await sendEmail(ctx.last.email, 'Welcome!');
  })
  .action('log-user-creation', async ctx => {
    await logUserCreation(ctx.last);
  });

// Avoid: Using steps for side effects when you don't need the output
workflow
  .step('create-user', async ctx => {
    return await createUser(ctx.payload);
  })
  .step('send-welcome-email', async ctx => {
    await sendEmail(ctx.last.email, 'Welcome!');
    return { emailSent: true }; // Unnecessary return
  });

2. Handle Action Errors Gracefully

typescript
workflow
  .step('process-data', async ctx => {
    return await processData(ctx.payload);
  })
  .action('send-notification', async ctx => {
    try {
      await sendNotification(ctx.last);
    } catch (error) {
      // Log error but don't fail the workflow
      console.error('Notification failed:', error);

      // Optionally retry or send to dead letter queue
      await deadLetterQueue.send({
        type: 'notification',
        data: ctx.last,
        error: error.message,
      });
    }
  });

3. Use Actions for Non-Critical Operations

typescript
workflow
  .step('process-payment', async ctx => {
    // Critical operation - use step
    return await processPayment(ctx.payload);
  })
  .action('send-payment-notification', async ctx => {
    // Non-critical - use action
    await sendPaymentNotification(ctx.last);
  })
  .action('update-analytics', async ctx => {
    // Non-critical - use action
    await updatePaymentAnalytics(ctx.last);
  })
  .action('cleanup-temp-data', async ctx => {
    // Non-critical - use action
    await cleanupTempData();
  });

4. Monitor Action Performance

typescript
workflow
  .step('process-data', async ctx => {
    return await processData(ctx.payload);
  })
  .action('monitored-action', async ctx => {
    const startTime = Date.now();

    try {
      await performAction(ctx.last);

      const duration = Date.now() - startTime;
      await metrics.timing('action.duration', duration);
      await metrics.increment('action.success');
    } catch (error) {
      await metrics.increment('action.failure');
      throw error;
    }
  });

5. Use Actions for Batch Operations

typescript
workflow
  .step('fetch-users', async ctx => {
    return await fetchUsers(ctx.payload.criteria);
  })
  .action('batch-process-users', async ctx => {
    const users = ctx.last;

    // Process users in batches
    for (let i = 0; i < users.length; i += 100) {
      const batch = users.slice(i, i + 100);
      await processUserBatch(batch);

      // Small delay between batches
      await new Promise(resolve => setTimeout(resolve, 100));
    }
  });

Action vs Step Decision Guide

Use Steps When:

  • You need the output for subsequent steps
  • The operation is critical to workflow success
  • You want to handle errors in the workflow
  • The operation affects the main business logic

Use Actions When:

  • You're performing side effects (logging, notifications)
  • The operation is non-critical
  • You don't need the output
  • You want fire-and-forget behavior
  • The operation is for monitoring or analytics

Released under the Apache 2.0 License.