martinni39
martinni39

Reputation: 323

How to create custom workflow definitions?

We have requirements to let our users create their own workflows. Those workflows can have simple yes / no branching as well as waiting for a signal from an external event. This wouldn’t be such a problem if we had well established workflow definition, however since the workflows can be dynamic this poses a much tricker problem.

Upvotes: 4

Views: 392

Answers (1)

Maxim Fateev
Maxim Fateev

Reputation: 6870

Temporal Workflows are code that directly implements your business logic.

For use cases when hardcoding the business logic in code is not an option an interpreter of an external workflow definition language should be written. Such language is frequently called DSL as they are really useful when implemented for a specific domain. The DSLs are frequently YAML/Json/XML based. Sometimes it is just data in DB tables.

Here is how I would structure the workflow code to support custom DSL:

  1. An activity that receives current workflow definition ID and state and returns a list of operations to execute. This activity applies the current state (which includes results to the most recently executed operations) to the appropriate DSL instance. The result is the set of next operations to execute. Operations are DSL specific, but most common ones are execute activity, wait for specific signal, sleep for some time, complete or fail workflow.
  2. A workflow that implements a loop that calls the above activity and executes requested operations until the workflow completion operation is requested.

Here is a sample code for a trivial DSL that specifies a sequence of activities to execute:

@ActivityInterface
public interface Interpreter {
  String getNextStep(String workflowType, String lastActivity);
}

public class SequenceInterpreter implements Interpreter {

  // dslWorkflowType->(activityType->nextActivity)
  private final Map<String, Map<String, String>> definitions;

  public SequenceInterpreter(Map<String, Map<String, String>> definitions) {
    this.definitions = definitions;
  }

  @Override
  public String getNextStep(String workflowType, String lastActivity) {
    Map<String, String> stateTransitions = definitions.get(workflowType);
    return stateTransitions.get(lastActivity);
  }
}

@WorkflowInterface    
public interface InterpreterWorkflow {
  @WorkflowMethod
  String execute(String type, String input);
  @QueryMethod
  String getCurrentActivity();
}

public class InterpreterWorkflowImpl implements InterpreterWorkflow {

  private final Interpreter interpreter = Workflow.newActivityStub(Interpreter.class);

  private final ActivityStub activities =
      Workflow.newUntypedActivityStub(
          new ActivityOptions.Builder().setScheduleToCloseTimeout(Duration.ofMinutes(10)).build());

  private String currentActivity = "init";
  private String lastActivityResult;

  @Override
  public String execute(String workflowType, String input) {
    do {
      currentActivity = interpreter.getNextStep(workflowType, currentActivity);
      lastActivityResult = activities.execute(currentActivity, String.class, lastActivityResult);
    } while (currentActivity != null);
    return lastActivityResult;
  }

  @Override
  public String getCurrentActivity() {
    return currentActivity;
  }
}

Obviously the real-life interpreter activity is going to receive a more complex state object as a parameter and return a structure that potentially contains a list of multiple command types.

Upvotes: 4

Related Questions