Creating a scheduler implementation

Scheduler implementations are responsible for delaying automatic workflow action execution. For example, you may have a workflow where you only move to the next step when something happens in the future. You may define that you wish to check this once a day for up to a year:

steps:
- id: paused.waiting
  autoActionTimers:
  - interval: 1d
    count: 365
  actions:
  - id: next
    condition:
      ref: custom.SomethingIsTrue
    defaultResult:
      transitions:
      - step: paused.waiting
        status: completed
      - step: continue.to.do.something
        status: active

# ...

The IWorkflowScheduler interface

Implementing a scheduler is achieved by creating a CFC that implements the cfflow.models.implementation.interfaces.IWorkflowScheduler interface and registering it with the CfFlow engine. The interface definition is as follows:

interface {

	public void function scheduleAutoActions(
		  required string workflowId
		, required struct instanceArgs
		, required string stepId
		, required array  timers
	);

	public void function unScheduleAutoActions(
		  required string workflowId
		, required struct instanceArgs
		, required string stepId
	);

}

Registering a scheduler

Registering a scheduler is achieved by using the cfflow.registerScheduler() method:

var myScheduler = new my.CustomScheduler();

cfflow.registerScheduler(
	    className      = "my.scheduler"
	  , implementation = myScheduler
);

Executing auto actions

Your scheduler will have access to a string workflowId, a struct of instanceArgs to get the relevant workflow instance and a string stepId. These variables can be used to execute the automatic actions on the specified step and workflow instance.

The example below, is a Coldbox handler that a custom scheduler invokes on each scheduled iteration:

component {

	property name="cfflow" inject="cfflow@cfflow";

	private boolean function runScheduledAutoActions( event, rc, prc, args={}, logger, progress ) {
		var wfId   = args.workflowId   ?: "";
		var iArgs  = args.instanceArgs ?: {};
		var stepId = args.stepId       ?: "";

		// does the instance stil exist?
		if ( !cfflow.instanceExists( workflowId=wfId, instanceArgs=iArgs ) ) {
			// return true, we should not try to run again
			return true;
		}

		// execute the auto actions
		try {
			// if no actions are executed, we will return false
			// and this task will be rescheduled if need be
			return cfflow.doAutoActions(
				  workflowId   = wfId
				, instanceArgs = iArgs
				, stepId       = stepId
			);

		} catch( "cfflow.step.not.active" e ) {
			// if the step is no longer active, we should return true to prevent rescheduling
			return true;
		}
	}
}

Scheduler example using Preside

The corresponding scheduler to the execution handler above, is as follows:

/**
 * @presideService true
 * @singleton      true
 */
component implements="cfflow.models.implementation.interfaces.IWorkflowScheduler" {

// CONSTRUCTOR
	public any function init() {
		return this;
	}

// PUBLIC API METHODS
	public void function scheduleAutoActions(
		  required string workflowId
		, required struct instanceArgs
		, required string stepId
		, required array  timers
	){
		var sched = _calculateSchedule( arguments.timers );

		// use preside ad-hoc task manager for the scheduler
		$createTask(
			  event             = "cfFlowHelpers.runScheduledAutoActions"
			, args              = { workflowId=arguments.workflowId, stepId=arguments.stepId, instanceArgs=arguments.instanceArgs }
			, runNow            = false
			, runIn             = sched.runIn
			, retryInterval     = sched.retries
			, discardOnComplete = true
		);
	}

	public void function unScheduleAutoActions(
		  required string workflowId
		, required struct instanceArgs
		, required string stepId
	){
		// not yet implemented and not necessary
		// we can stop the schedule when the step
		// becomes unavailable
	}

// PRIVATE HELPERS
	private struct function _calculateSchedule( required array timers ) {
		var sched = {
			  runIn = CreateTimeSpan( 0, 0, 0, arguments.timers[ 1 ].getInterval() )
			, retries = []
		};

		var start = ( arguments.timers[ 1 ].getCount() == 1 ) ? 2 : 1;
		for( var i=start; i<=ArrayLen( arguments.timers ); i++ ) {
			var retry = {
				  tries    = arguments.timers[ i ].getCount()
				, interval = arguments.timers[ i ].getInterval()
			};
			if ( i==1 ) {
				retry.tries--;
			}

			ArrayAppend( sched.retries, retry );
		}

		return sched;
	}
}