Input:
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.netflix.conductor.common.metadata.tasks.TaskDef;
import com.netflix.conductor.common.metadata.tasks.TaskType;
import com.netflix.conductor.common.metadata.workflow.WorkflowDef;
import com.netflix.conductor.common.metadata.workflow.WorkflowTask;
import com.netflix.conductor.core.exception.TerminateWorkflowException;
import com.netflix.conductor.core.utils.ParametersUtils;
import com.netflix.conductor.model.TaskModel;
import com.netflix.conductor.model.WorkflowModel;

Context:
core/src/main/java/com/netflix/conductor/core/execution/mapper/SimpleTaskMapper.java
/*
 * Copyright 2022 Conductor Authors.
 *

 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *

 * http://www.apache.org/licenses/LICENSE-2.0
 *

 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.netflix.conductor.core.execution.mapper;

/**
 * An implementation of {@link TaskMapper} to map a {@link WorkflowTask} of type {@link
 * TaskType#SIMPLE} to a {@link TaskModel} with status {@link TaskModel.Status#SCHEDULED}.
 * NOTE: There is not type defined for simples task.
 */
@Component
public class SimpleTaskMapper implements TaskMapper {

    public static final Logger LOGGER = LoggerFactory.getLogger(SimpleTaskMapper.class);
    private final ParametersUtils parametersUtils;

    public SimpleTaskMapper(ParametersUtils parametersUtils) {
        this.parametersUtils = parametersUtils;
    }

    @Override
    public String getTaskType() {

common/src/main/java/com/netflix/conductor/common/metadata/workflow/WorkflowDef.java
@ProtoMessage
@TaskReferenceNameUniqueConstraint
public class WorkflowDef extends BaseDef {

    @ProtoEnum
    public enum TimeoutPolicy {
        TIME_OUT_WF,
        ALERT_ONLY
    }

    @NotEmpty(message = "WorkflowDef name cannot be null or empty")
    @ProtoField(id = 1)
    @NoSemiColonConstraint(
            message = "Workflow name cannot contain the following set of characters: ':'")
    private String name;

    @ProtoField(id = 2)
    private String description;

    @ProtoField(id = 3)
    private int version = 1;

    @ProtoField(id = 4)
    @NotNull
    @NotEmpty(message = "WorkflowTask list cannot be empty")
    private List<@Valid WorkflowTask> tasks = new LinkedList<>();

    @ProtoField(id = 5)
    private List<String> inputParameters = new LinkedList<>();

    @ProtoField(id = 6)
    private Map<String, Object> outputParameters = new HashMap<>();

    @ProtoField(id = 7)
    private String failureWorkflow;

    @ProtoField(id = 8)
    @Min(value = 2, message = "workflowDef schemaVersion: {value} is only supported")
    @Max(value = 2, message = "workflowDef schemaVersion: {value} is only supported")
    private int schemaVersion = 2;

    // By default, a workflow is restartable
    @ProtoField(id = 9)
    private boolean restartable = true;

    @ProtoField(id = 10)
    private boolean workflowStatusListenerEnabled = false;

    @ProtoField(id = 11)
    @OwnerEmailMandatoryConstraint
    @Email(message = "ownerEmail should be valid email address")
    private String ownerEmail;

    @ProtoField(id = 12)
    private TimeoutPolicy timeoutPolicy = TimeoutPolicy.ALERT_ONLY;

    @ProtoField(id = 13)
    @NotNull
    private long timeoutSeconds;

    @ProtoField(id = 14)
    private Map<String, Object> variables = new HashMap<>();

    @ProtoField(id = 15)
    private Map<String, Object> inputTemplate = new HashMap<>();

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the description
     */
    public String getDescription() {
        return description;
    }

    /**
     * @param description the description to set
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * @return the tasks
     */
    public List<WorkflowTask> getTasks() {
        return tasks;
    }

    /**
     * @param tasks the tasks to set
     */
    public void setTasks(List<@Valid WorkflowTask> tasks) {
        this.tasks = tasks;
    }

    /**
     * @return the inputParameters
     */
    public List<String> getInputParameters() {
        return inputParameters;
    }

    /**
     * @param inputParameters the inputParameters to set
     */
    public void setInputParameters(List<String> inputParameters) {
        this.inputParameters = inputParameters;
    }

    /**
     * @return the outputParameters
     */
    public Map<String, Object> getOutputParameters() {
        return outputParameters;
    }

    /**
     * @param outputParameters the outputParameters to set
     */
    public void setOutputParameters(Map<String, Object> outputParameters) {
        this.outputParameters = outputParameters;
    }

    /**
     * @return the version
     */
    public int getVersion() {
        return version;
    }

    /**
     * @return the failureWorkflow
     */
    public String getFailureWorkflow() {
        return failureWorkflow;
    }

    /**
     * @param failureWorkflow the failureWorkflow to set
     */
    public void setFailureWorkflow(String failureWorkflow) {
        this.failureWorkflow = failureWorkflow;
    }

    /**
     * @param version the version to set
     */
    public void setVersion(int version) {
        this.version = version;
    }

    /**
     * This method determines if the workflow is restartable or not
     *
     * @return true: if the workflow is restartable false: if the workflow is non restartable
     */
    public boolean isRestartable() {
        return restartable;
    }

    /**
     * This method is called only when the workflow definition is created
     *
     * @param restartable true: if the workflow is restartable false: if the workflow is non
     *     restartable
     */
    public void setRestartable(boolean restartable) {
        this.restartable = restartable;
    }

    /**
     * @return the schemaVersion
     */
    public int getSchemaVersion() {
        return schemaVersion;
    }

    /**
     * @param schemaVersion the schemaVersion to set
     */
    public void setSchemaVersion(int schemaVersion) {
        this.schemaVersion = schemaVersion;
    }

    /**
     * @return true is workflow listener will be invoked when workflow gets into a terminal state
     */
    public boolean isWorkflowStatusListenerEnabled() {
        return workflowStatusListenerEnabled;
    }

    /**
     * Specify if workflow listener is enabled to invoke a callback for completed or terminated
     * workflows
     *
     * @param workflowStatusListenerEnabled
     */
    public void setWorkflowStatusListenerEnabled(boolean workflowStatusListenerEnabled) {
        this.workflowStatusListenerEnabled = workflowStatusListenerEnabled;
    }

    /**
     * @return the email of the owner of this workflow definition
     */
    public String getOwnerEmail() {
        return ownerEmail;
    }

    /**
     * @param ownerEmail the owner email to set
     */
    public void setOwnerEmail(String ownerEmail) {
        this.ownerEmail = ownerEmail;
    }

    /**
     * @return the timeoutPolicy
     */
    public TimeoutPolicy getTimeoutPolicy() {
        return timeoutPolicy;
    }

    /**
     * @param timeoutPolicy the timeoutPolicy to set
     */
    public void setTimeoutPolicy(TimeoutPolicy timeoutPolicy) {
        this.timeoutPolicy = timeoutPolicy;
    }

    /**
     * @return the time after which a workflow is deemed to have timed out
     */
    public long getTimeoutSeconds() {
        return timeoutSeconds;
    }

    /**
     * @param timeoutSeconds the timeout in seconds to set
     */
    public void setTimeoutSeconds(long timeoutSeconds) {
        this.timeoutSeconds = timeoutSeconds;
    }

    /**
     * @return the global workflow variables
     */
    public Map<String, Object> getVariables() {
        return variables;
    }

    /**
     * @param variables the set of global workflow variables to set
     */
    public void setVariables(Map<String, Object> variables) {
        this.variables = variables;
    }

    public Map<String, Object> getInputTemplate() {
        return inputTemplate;
    }

    public void setInputTemplate(Map<String, Object> inputTemplate) {
        this.inputTemplate = inputTemplate;
    }

    public String key() {
        return getKey(name, version);
    }

    public static String getKey(String name, int version) {
        return name + "." + version;
    }

    public boolean containsType(String taskType) {
        return collectTasks().stream().anyMatch(t -> t.getType().equals(taskType));
    }

    public WorkflowTask getNextTask(String taskReferenceName) {
        WorkflowTask workflowTask = getTaskByRefName(taskReferenceName);
        if (workflowTask != null && TaskType.TERMINATE.name().equals(workflowTask.getType())) {
            return null;
        }

        Iterator<WorkflowTask> iterator = tasks.iterator();
        while (iterator.hasNext()) {
            WorkflowTask task = iterator.next();
            if (task.getTaskReferenceName().equals(taskReferenceName)) {
                // If taskReferenceName matches, break out
                break;
            }
            WorkflowTask nextTask = task.next(taskReferenceName, null);
            if (nextTask != null) {
                return nextTask;
            } else if (TaskType.DO_WHILE.name().equals(task.getType())
                    && !task.getTaskReferenceName().equals(taskReferenceName)
                    && task.has(taskReferenceName)) {
                // If the task is child of Loop Task and at last position, return null.
                return null;
            }

            if (task.has(taskReferenceName)) {
                break;
            }
        }
        if (iterator.hasNext()) {
            return iterator.next();
        }
        return null;
    }

    public WorkflowTask getTaskByRefName(String taskReferenceName) {
        return collectTasks().stream()
                .filter(
                        workflowTask ->
                                workflowTask.getTaskReferenceName().equals(taskReferenceName))
                .findFirst()
                .orElse(null);
    }

    public List<WorkflowTask> collectTasks() {
        List<WorkflowTask> tasks = new LinkedList<>();
        for (WorkflowTask workflowTask : this.tasks) {
            tasks.addAll(workflowTask.collectTasks());
        }
        return tasks;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        WorkflowDef that = (WorkflowDef) o;
        return getVersion() == that.getVersion()
                && getSchemaVersion() == that.getSchemaVersion()
                && Objects.equals(getName(), that.getName())
                && Objects.equals(getDescription(), that.getDescription())
                && Objects.equals(getTasks(), that.getTasks())
                && Objects.equals(getInputParameters(), that.getInputParameters())
                && Objects.equals(getOutputParameters(), that.getOutputParameters())
                && Objects.equals(getFailureWorkflow(), that.getFailureWorkflow())
                && Objects.equals(getOwnerEmail(), that.getOwnerEmail())
                && Objects.equals(getTimeoutSeconds(), that.getTimeoutSeconds());
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                getName(),
                getDescription(),
                getVersion(),
                getTasks(),
                getInputParameters(),
                getOutputParameters(),
                getFailureWorkflow(),
                getSchemaVersion(),
                getOwnerEmail(),
                getTimeoutSeconds());
    }

    @Override
    public String toString() {
        return "WorkflowDef{"
                + "name='"
                + name
                + '\''
                + ", description='"
                + description
                + '\''
                + ", version="
                + version
                + ", tasks="
                + tasks
                + ", inputParameters="
                + inputParameters
                + ", outputParameters="
                + outputParameters
                + ", failureWorkflow='"
                + failureWorkflow
                + '\''
                + ", schemaVersion="
                + schemaVersion
                + ", restartable="
                + restartable
                + ", workflowStatusListenerEnabled="
                + workflowStatusListenerEnabled
                + ", timeoutSeconds="
                + timeoutSeconds
                + '}';
    }
}

core/src/main/java/com/netflix/conductor/core/exception/TerminateWorkflowException.java
public class TerminateWorkflowException extends RuntimeException {

    private final WorkflowModel.Status workflowStatus;
    private final TaskModel task;

    public TerminateWorkflowException(String reason) {
        this(reason, FAILED);
    }

    public TerminateWorkflowException(String reason, WorkflowModel.Status workflowStatus) {
        this(reason, workflowStatus, null);
    }

    public TerminateWorkflowException(
            String reason, WorkflowModel.Status workflowStatus, TaskModel task) {
        super(reason);
        this.workflowStatus = workflowStatus;
        this.task = task;
    }

    public WorkflowModel.Status getWorkflowStatus() {
        return workflowStatus;
    }

    public TaskModel getTask() {
        return task;
    }
}

common/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskDef.java
@ProtoMessage
@TaskTimeoutConstraint
@Valid
public class TaskDef extends BaseDef {

    @ProtoEnum
    public enum TimeoutPolicy {
        RETRY,
        TIME_OUT_WF,
        ALERT_ONLY
    }

    @ProtoEnum
    public enum RetryLogic {
        FIXED,
        EXPONENTIAL_BACKOFF,
        LINEAR_BACKOFF
    }

    public static final int ONE_HOUR = 60 * 60;

    /** Unique name identifying the task. The name is unique across */
    @NotEmpty(message = "TaskDef name cannot be null or empty")
    @ProtoField(id = 1)
    private String name;

    @ProtoField(id = 2)
    private String description;

    @ProtoField(id = 3)
    @Min(value = 0, message = "TaskDef retryCount: {value} must be >= 0")
    private int retryCount = 3; // Default

    @ProtoField(id = 4)
    @NotNull
    private long timeoutSeconds;

    @ProtoField(id = 5)
    private List<String> inputKeys = new ArrayList<>();

    @ProtoField(id = 6)
    private List<String> outputKeys = new ArrayList<>();

    @ProtoField(id = 7)
    private TimeoutPolicy timeoutPolicy = TimeoutPolicy.TIME_OUT_WF;

    @ProtoField(id = 8)
    private RetryLogic retryLogic = RetryLogic.FIXED;

    @ProtoField(id = 9)
    private int retryDelaySeconds = 60;

    @ProtoField(id = 10)
    @Min(
            value = 1,
            message =
                    "TaskDef responseTimeoutSeconds: ${validatedValue} should be minimum {value} second")
    private long responseTimeoutSeconds = ONE_HOUR;

    @ProtoField(id = 11)
    private Integer concurrentExecLimit;

    @ProtoField(id = 12)
    private Map<String, Object> inputTemplate = new HashMap<>();

    // This field is deprecated, do not use id 13.
    //	@ProtoField(id = 13)
    //	private Integer rateLimitPerSecond;

    @ProtoField(id = 14)
    private Integer rateLimitPerFrequency;

    @ProtoField(id = 15)
    private Integer rateLimitFrequencyInSeconds;

    @ProtoField(id = 16)
    private String isolationGroupId;

    @ProtoField(id = 17)
    private String executionNameSpace;

    @ProtoField(id = 18)
    @OwnerEmailMandatoryConstraint
    @Email(message = "ownerEmail should be valid email address")
    private String ownerEmail;

    @ProtoField(id = 19)
    @Min(value = 0, message = "TaskDef pollTimeoutSeconds: {value} must be >= 0")
    private Integer pollTimeoutSeconds;

    @ProtoField(id = 20)
    @Min(value = 1, message = "Backoff scale factor. Applicable for LINEAR_BACKOFF")
    private Integer backoffScaleFactor = 1;

    public TaskDef() {}

    public TaskDef(String name) {
        this.name = name;
    }

    public TaskDef(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public TaskDef(String name, String description, int retryCount, long timeoutSeconds) {
        this.name = name;
        this.description = description;
        this.retryCount = retryCount;
        this.timeoutSeconds = timeoutSeconds;
    }

    public TaskDef(
            String name,
            String description,
            String ownerEmail,
            int retryCount,
            long timeoutSeconds,
            long responseTimeoutSeconds) {
        this.name = name;
        this.description = description;
        this.ownerEmail = ownerEmail;
        this.retryCount = retryCount;
        this.timeoutSeconds = timeoutSeconds;
        this.responseTimeoutSeconds = responseTimeoutSeconds;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the description
     */
    public String getDescription() {
        return description;
    }

    /**
     * @param description the description to set
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * @return the retryCount
     */
    public int getRetryCount() {
        return retryCount;
    }

    /**
     * @param retryCount the retryCount to set
     */
    public void setRetryCount(int retryCount) {
        this.retryCount = retryCount;
    }

    /**
     * @return the timeoutSeconds
     */
    public long getTimeoutSeconds() {
        return timeoutSeconds;
    }

    /**
     * @param timeoutSeconds the timeoutSeconds to set
     */
    public void setTimeoutSeconds(long timeoutSeconds) {
        this.timeoutSeconds = timeoutSeconds;
    }

    /**
     * @return Returns the input keys
     */
    public List<String> getInputKeys() {
        return inputKeys;
    }

    /**
     * @param inputKeys Set of keys that the task accepts in the input map
     */
    public void setInputKeys(List<String> inputKeys) {
        this.inputKeys = inputKeys;
    }

    /**
     * @return Returns the output keys for the task when executed
     */
    public List<String> getOutputKeys() {
        return outputKeys;
    }

    /**
     * @param outputKeys Sets the output keys
     */
    public void setOutputKeys(List<String> outputKeys) {
        this.outputKeys = outputKeys;
    }

    /**
     * @return the timeoutPolicy
     */
    public TimeoutPolicy getTimeoutPolicy() {
        return timeoutPolicy;
    }

    /**
     * @param timeoutPolicy the timeoutPolicy to set
     */
    public void setTimeoutPolicy(TimeoutPolicy timeoutPolicy) {
        this.timeoutPolicy = timeoutPolicy;
    }

    /**
     * @return the retryLogic
     */
    public RetryLogic getRetryLogic() {
        return retryLogic;
    }

    /**
     * @param retryLogic the retryLogic to set
     */
    public void setRetryLogic(RetryLogic retryLogic) {
        this.retryLogic = retryLogic;
    }

    /**
     * @return the retryDelaySeconds
     */
    public int getRetryDelaySeconds() {
        return retryDelaySeconds;
    }

    /**
     * @return the timeout for task to send response. After this timeout, the task will be re-queued
     */
    public long getResponseTimeoutSeconds() {
        return responseTimeoutSeconds;
    }

    /**
     * @param responseTimeoutSeconds - timeout for task to send response. After this timeout, the
     *     task will be re-queued
     */
    public void setResponseTimeoutSeconds(long responseTimeoutSeconds) {
        this.responseTimeoutSeconds = responseTimeoutSeconds;
    }

    /**
     * @param retryDelaySeconds the retryDelaySeconds to set
     */
    public void setRetryDelaySeconds(int retryDelaySeconds) {
        this.retryDelaySeconds = retryDelaySeconds;
    }

    /**
     * @return the inputTemplate
     */
    public Map<String, Object> getInputTemplate() {
        return inputTemplate;
    }

    /**
     * @return rateLimitPerFrequency The max number of tasks that will be allowed to be executed per
     *     rateLimitFrequencyInSeconds.
     */
    public Integer getRateLimitPerFrequency() {
        return rateLimitPerFrequency == null ? 0 : rateLimitPerFrequency;
    }

    /**
     * @param rateLimitPerFrequency The max number of tasks that will be allowed to be executed per
     *     rateLimitFrequencyInSeconds. Setting the value to 0 removes the rate limit
     */
    public void setRateLimitPerFrequency(Integer rateLimitPerFrequency) {
        this.rateLimitPerFrequency = rateLimitPerFrequency;
    }

    /**
     * @return rateLimitFrequencyInSeconds: The time bucket that is used to rate limit tasks based
     *     on {@link #getRateLimitPerFrequency()} If null or not set, then defaults to 1 second
     */
    public Integer getRateLimitFrequencyInSeconds() {
        return rateLimitFrequencyInSeconds == null ? 1 : rateLimitFrequencyInSeconds;
    }

    /**
     * @param rateLimitFrequencyInSeconds: The time window/bucket for which the rate limit needs to
     *     be applied. This will only have affect if {@link #getRateLimitPerFrequency()} is greater
     *     than zero
     */
    public void setRateLimitFrequencyInSeconds(Integer rateLimitFrequencyInSeconds) {
        this.rateLimitFrequencyInSeconds = rateLimitFrequencyInSeconds;
    }

    /**
     * @param concurrentExecLimit Limit of number of concurrent task that can be IN_PROGRESS at a
     *     given time. Seting the value to 0 removes the limit.
     */
    public void setConcurrentExecLimit(Integer concurrentExecLimit) {
        this.concurrentExecLimit = concurrentExecLimit;
    }

    /**
     * @return Limit of number of concurrent task that can be IN_PROGRESS at a given time
     */
    public Integer getConcurrentExecLimit() {
        return concurrentExecLimit;
    }

    /**
     * @return concurrency limit
     */
    public int concurrencyLimit() {
        return concurrentExecLimit == null ? 0 : concurrentExecLimit;
    }

    /**
     * @param inputTemplate the inputTemplate to set
     */
    public void setInputTemplate(Map<String, Object> inputTemplate) {
        this.inputTemplate = inputTemplate;
    }

    public String getIsolationGroupId() {
        return isolationGroupId;
    }

    public void setIsolationGroupId(String isolationGroupId) {
        this.isolationGroupId = isolationGroupId;
    }

    public String getExecutionNameSpace() {
        return executionNameSpace;
    }

    public void setExecutionNameSpace(String executionNameSpace) {
        this.executionNameSpace = executionNameSpace;
    }

    /**
     * @return the email of the owner of this task definition
     */
    public String getOwnerEmail() {
        return ownerEmail;
    }

    /**
     * @param ownerEmail the owner email to set
     */
    public void setOwnerEmail(String ownerEmail) {
        this.ownerEmail = ownerEmail;
    }

    /**
     * @param pollTimeoutSeconds the poll timeout to set
     */
    public void setPollTimeoutSeconds(Integer pollTimeoutSeconds) {
        this.pollTimeoutSeconds = pollTimeoutSeconds;
    }

    /**
     * @return the poll timeout of this task definition
     */
    public Integer getPollTimeoutSeconds() {
        return pollTimeoutSeconds;
    }

    /**
     * @param backoffScaleFactor the backoff rate to set
     */
    public void setBackoffScaleFactor(Integer backoffScaleFactor) {
        this.backoffScaleFactor = backoffScaleFactor;
    }

    /**
     * @return the backoff rate of this task definition
     */
    public Integer getBackoffScaleFactor() {
        return backoffScaleFactor;
    }

    @Override
    public String toString() {
        return name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        TaskDef taskDef = (TaskDef) o;
        return getRetryCount() == taskDef.getRetryCount()
                && getTimeoutSeconds() == taskDef.getTimeoutSeconds()
                && getRetryDelaySeconds() == taskDef.getRetryDelaySeconds()
                && getBackoffScaleFactor() == taskDef.getBackoffScaleFactor()
                && getResponseTimeoutSeconds() == taskDef.getResponseTimeoutSeconds()
                && Objects.equals(getName(), taskDef.getName())
                && Objects.equals(getDescription(), taskDef.getDescription())
                && Objects.equals(getInputKeys(), taskDef.getInputKeys())
                && Objects.equals(getOutputKeys(), taskDef.getOutputKeys())
                && getTimeoutPolicy() == taskDef.getTimeoutPolicy()
                && getRetryLogic() == taskDef.getRetryLogic()
                && Objects.equals(getConcurrentExecLimit(), taskDef.getConcurrentExecLimit())
                && Objects.equals(getRateLimitPerFrequency(), taskDef.getRateLimitPerFrequency())
                && Objects.equals(getInputTemplate(), taskDef.getInputTemplate())
                && Objects.equals(getIsolationGroupId(), taskDef.getIsolationGroupId())
                && Objects.equals(getExecutionNameSpace(), taskDef.getExecutionNameSpace())
                && Objects.equals(getOwnerEmail(), taskDef.getOwnerEmail());
    }

    @Override
    public int hashCode() {

        return Objects.hash(
                getName(),
                getDescription(),
                getRetryCount(),
                getTimeoutSeconds(),
                getInputKeys(),
                getOutputKeys(),
                getTimeoutPolicy(),
                getRetryLogic(),
                getRetryDelaySeconds(),
                getBackoffScaleFactor(),
                getResponseTimeoutSeconds(),
                getConcurrentExecLimit(),
                getRateLimitPerFrequency(),
                getInputTemplate(),
                getIsolationGroupId(),
                getExecutionNameSpace(),
                getOwnerEmail());
    }
}

common/src/main/java/com/netflix/conductor/common/metadata/workflow/WorkflowTask.java
@ProtoMessage
public class WorkflowTask {

    @ProtoField(id = 1)
    @NotEmpty(message = "WorkflowTask name cannot be empty or null")
    private String name;

    @ProtoField(id = 2)
    @NotEmpty(message = "WorkflowTask taskReferenceName name cannot be empty or null")
    private String taskReferenceName;

    @ProtoField(id = 3)
    private String description;

    @ProtoField(id = 4)
    private Map<String, Object> inputParameters = new HashMap<>();

    @ProtoField(id = 5)
    private String type = TaskType.SIMPLE.name();

    @ProtoField(id = 6)
    private String dynamicTaskNameParam;

    @Deprecated
    @ProtoField(id = 7)
    private String caseValueParam;

    @Deprecated
    @ProtoField(id = 8)
    private String caseExpression;

    @ProtoField(id = 22)
    private String scriptExpression;

    @ProtoMessage(wrapper = true)
    public static class WorkflowTaskList {

        public List<WorkflowTask> getTasks() {
            return tasks;
        }

        public void setTasks(List<WorkflowTask> tasks) {
            this.tasks = tasks;
        }

        @ProtoField(id = 1)
        private List<WorkflowTask> tasks;
    }

    // Populates for the tasks of the decision type
    @ProtoField(id = 9)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private Map<String, @Valid List<@Valid WorkflowTask>> decisionCases = new LinkedHashMap<>();

    @Deprecated private String dynamicForkJoinTasksParam;

    @ProtoField(id = 10)
    private String dynamicForkTasksParam;

    @ProtoField(id = 11)
    private String dynamicForkTasksInputParamName;

    @ProtoField(id = 12)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<@Valid WorkflowTask> defaultCase = new LinkedList<>();

    @ProtoField(id = 13)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<@Valid List<@Valid WorkflowTask>> forkTasks = new LinkedList<>();

    @ProtoField(id = 14)
    @PositiveOrZero
    private int startDelay; // No. of seconds (at-least) to wait before starting a task.

    @ProtoField(id = 15)
    @Valid
    private SubWorkflowParams subWorkflowParam;

    @ProtoField(id = 16)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<String> joinOn = new LinkedList<>();

    @ProtoField(id = 17)
    private String sink;

    @ProtoField(id = 18)
    private boolean optional = false;

    @ProtoField(id = 19)
    private TaskDef taskDefinition;

    @ProtoField(id = 20)
    private Boolean rateLimited;

    @ProtoField(id = 21)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<String> defaultExclusiveJoinTask = new LinkedList<>();

    @ProtoField(id = 23)
    private Boolean asyncComplete = false;

    @ProtoField(id = 24)
    private String loopCondition;

    @ProtoField(id = 25)
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<WorkflowTask> loopOver = new LinkedList<>();

    @ProtoField(id = 26)
    private Integer retryCount;

    @ProtoField(id = 27)
    private String evaluatorType;

    @ProtoField(id = 28)
    private String expression;

    @ProtoField(id = 29)
    private boolean permissive = false;

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the taskReferenceName
     */
    public String getTaskReferenceName() {
        return taskReferenceName;
    }

    /**
     * @param taskReferenceName the taskReferenceName to set
     */
    public void setTaskReferenceName(String taskReferenceName) {
        this.taskReferenceName = taskReferenceName;
    }

    /**
     * @return the description
     */
    public String getDescription() {
        return description;
    }

    /**
     * @param description the description to set
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * @return the inputParameters
     */
    public Map<String, Object> getInputParameters() {
        return inputParameters;
    }

    /**
     * @param inputParameters the inputParameters to set
     */
    public void setInputParameters(Map<String, Object> inputParameters) {
        this.inputParameters = inputParameters;
    }

    /**
     * @return the type
     */
    public String getType() {
        return type;
    }

    public void setWorkflowTaskType(TaskType type) {
        this.type = type.name();
    }

    /**
     * @param type the type to set
     */
    public void setType(@NotEmpty(message = "WorkTask type cannot be null or empty") String type) {
        this.type = type;
    }

    /**
     * @return the decisionCases
     */
    public Map<String, List<WorkflowTask>> getDecisionCases() {
        return decisionCases;
    }

    /**
     * @param decisionCases the decisionCases to set
     */
    public void setDecisionCases(Map<String, List<WorkflowTask>> decisionCases) {
        this.decisionCases = decisionCases;
    }

    /**
     * @return the defaultCase
     */
    public List<WorkflowTask> getDefaultCase() {
        return defaultCase;
    }

    /**
     * @param defaultCase the defaultCase to set
     */
    public void setDefaultCase(List<WorkflowTask> defaultCase) {
        this.defaultCase = defaultCase;
    }

    /**
     * @return the forkTasks
     */
    public List<List<WorkflowTask>> getForkTasks() {
        return forkTasks;
    }

    /**
     * @param forkTasks the forkTasks to set
     */
    public void setForkTasks(List<List<WorkflowTask>> forkTasks) {
        this.forkTasks = forkTasks;
    }

    /**
     * @return the startDelay in seconds
     */
    public int getStartDelay() {
        return startDelay;
    }

    /**
     * @param startDelay the startDelay to set
     */
    public void setStartDelay(int startDelay) {
        this.startDelay = startDelay;
    }

    /**
     * @return the retryCount
     */
    public Integer getRetryCount() {
        return retryCount;
    }

    /**
     * @param retryCount the retryCount to set
     */
    public void setRetryCount(final Integer retryCount) {
        this.retryCount = retryCount;
    }

    /**
     * @return the dynamicTaskNameParam
     */
    public String getDynamicTaskNameParam() {
        return dynamicTaskNameParam;
    }

    /**
     * @param dynamicTaskNameParam the dynamicTaskNameParam to set to be used by DYNAMIC tasks
     */
    public void setDynamicTaskNameParam(String dynamicTaskNameParam) {
        this.dynamicTaskNameParam = dynamicTaskNameParam;
    }

    /**
     * @deprecated Use {@link WorkflowTask#getEvaluatorType()} and {@link
     *     WorkflowTask#getExpression()} combination.
     * @return the caseValueParam
     */
    @Deprecated
    public String getCaseValueParam() {
        return caseValueParam;
    }

    @Deprecated
    public String getDynamicForkJoinTasksParam() {
        return dynamicForkJoinTasksParam;
    }

    @Deprecated
    public void setDynamicForkJoinTasksParam(String dynamicForkJoinTasksParam) {
        this.dynamicForkJoinTasksParam = dynamicForkJoinTasksParam;
    }

    public String getDynamicForkTasksParam() {
        return dynamicForkTasksParam;
    }

    public void setDynamicForkTasksParam(String dynamicForkTasksParam) {
        this.dynamicForkTasksParam = dynamicForkTasksParam;
    }

    public String getDynamicForkTasksInputParamName() {
        return dynamicForkTasksInputParamName;
    }

    public void setDynamicForkTasksInputParamName(String dynamicForkTasksInputParamName) {
        this.dynamicForkTasksInputParamName = dynamicForkTasksInputParamName;
    }

    /**
     * @param caseValueParam the caseValueParam to set
     * @deprecated Use {@link WorkflowTask#getEvaluatorType()} and {@link
     *     WorkflowTask#getExpression()} combination.
     */
    @Deprecated
    public void setCaseValueParam(String caseValueParam) {
        this.caseValueParam = caseValueParam;
    }

    /**
     * @return A javascript expression for decision cases. The result should be a scalar value that
     *     is used to decide the case branches.
     * @see #getDecisionCases()
     * @deprecated Use {@link WorkflowTask#getEvaluatorType()} and {@link
     *     WorkflowTask#getExpression()} combination.
     */
    @Deprecated
    public String getCaseExpression() {
        return caseExpression;
    }

    /**
     * @param caseExpression A javascript expression for decision cases. The result should be a
     *     scalar value that is used to decide the case branches.
     * @deprecated Use {@link WorkflowTask#getEvaluatorType()} and {@link
     *     WorkflowTask#getExpression()} combination.
     */
    @Deprecated
    public void setCaseExpression(String caseExpression) {
        this.caseExpression = caseExpression;
    }

    public String getScriptExpression() {
        return scriptExpression;
    }

    public void setScriptExpression(String expression) {
        this.scriptExpression = expression;
    }

    /**
     * @return the subWorkflow
     */
    public SubWorkflowParams getSubWorkflowParam() {
        return subWorkflowParam;
    }

    /**
     * @param subWorkflow the subWorkflowParam to set
     */
    public void setSubWorkflowParam(SubWorkflowParams subWorkflow) {
        this.subWorkflowParam = subWorkflow;
    }

    /**
     * @return the joinOn
     */
    public List<String> getJoinOn() {
        return joinOn;
    }

    /**
     * @param joinOn the joinOn to set
     */
    public void setJoinOn(List<String> joinOn) {
        this.joinOn = joinOn;
    }

    /**
     * @return the loopCondition
     */
    public String getLoopCondition() {
        return loopCondition;
    }

    /**
     * @param loopCondition the expression to set
     */
    public void setLoopCondition(String loopCondition) {
        this.loopCondition = loopCondition;
    }

    /**
     * @return the loopOver
     */
    public List<WorkflowTask> getLoopOver() {
        return loopOver;
    }

    /**
     * @param loopOver the loopOver to set
     */
    public void setLoopOver(List<WorkflowTask> loopOver) {
        this.loopOver = loopOver;
    }

    /**
     * @return Sink value for the EVENT type of task
     */
    public String getSink() {
        return sink;
    }

    /**
     * @param sink Name of the sink
     */
    public void setSink(String sink) {
        this.sink = sink;
    }

    /**
     * @return whether wait for an external event to complete the task, for EVENT and HTTP tasks
     */
    public Boolean isAsyncComplete() {
        return asyncComplete;
    }

    public void setAsyncComplete(Boolean asyncComplete) {
        this.asyncComplete = asyncComplete;
    }

    /**
     * @return If the task is optional. When set to true, the workflow execution continues even when
     *     the task is in failed status.
     */
    public boolean isOptional() {
        return optional;
    }

    /**
     * @return Task definition associated to the Workflow Task
     */
    public TaskDef getTaskDefinition() {
        return taskDefinition;
    }

    /**
     * @param taskDefinition Task definition
     */
    public void setTaskDefinition(TaskDef taskDefinition) {
        this.taskDefinition = taskDefinition;
    }

    /**
     * @param optional when set to true, the task is marked as optional
     */
    public void setOptional(boolean optional) {
        this.optional = optional;
    }

    public Boolean getRateLimited() {
        return rateLimited;
    }

    public void setRateLimited(Boolean rateLimited) {
        this.rateLimited = rateLimited;
    }

    public Boolean isRateLimited() {
        return rateLimited != null && rateLimited;
    }

    public List<String> getDefaultExclusiveJoinTask() {
        return defaultExclusiveJoinTask;
    }

    public void setDefaultExclusiveJoinTask(List<String> defaultExclusiveJoinTask) {
        this.defaultExclusiveJoinTask = defaultExclusiveJoinTask;
    }

    /**
     * @return the evaluatorType
     */
    public String getEvaluatorType() {
        return evaluatorType;
    }

    /**
     * @param evaluatorType the evaluatorType to set
     */
    public void setEvaluatorType(String evaluatorType) {
        this.evaluatorType = evaluatorType;
    }

    /**
     * @return An evaluation expression for switch cases evaluated by corresponding evaluator. The
     *     result should be a scalar value that is used to decide the case branches.
     * @see #getDecisionCases()
     */
    public String getExpression() {
        return expression;
    }

    /**
     * @param expression the expression to set
     */
    public void setExpression(String expression) {
        this.expression = expression;
    }

    /**
     * @return If the task is permissive. When set to true, and the task is in failed status,
     *     fail-fast does not occur. The workflow execution continues until reaching join or end of
     *     workflow, allowing idempotent execution of other tasks.
     */
    public boolean isPermissive() {
        return this.permissive;
    }

    /**
     * @param permissive when set to true, the task is marked as permissive
     */
    public void setPermissive(boolean permissive) {
        this.permissive = permissive;
    }

    private Collection<List<WorkflowTask>> children() {
        Collection<List<WorkflowTask>> workflowTaskLists = new LinkedList<>();

        switch (TaskType.of(type)) {
            case DECISION:
            case SWITCH:
                workflowTaskLists.addAll(decisionCases.values());
                workflowTaskLists.add(defaultCase);
                break;
            case FORK_JOIN:
                workflowTaskLists.addAll(forkTasks);
                break;
            case DO_WHILE:
                workflowTaskLists.add(loopOver);
                break;
            default:
                break;
        }
        return workflowTaskLists;
    }

    public List<WorkflowTask> collectTasks() {
        List<WorkflowTask> tasks = new LinkedList<>();
        tasks.add(this);
        for (List<WorkflowTask> workflowTaskList : children()) {
            for (WorkflowTask workflowTask : workflowTaskList) {
                tasks.addAll(workflowTask.collectTasks());
            }
        }
        return tasks;
    }

    public WorkflowTask next(String taskReferenceName, WorkflowTask parent) {
        TaskType taskType = TaskType.of(type);

        switch (taskType) {
            case DO_WHILE:
            case DECISION:
            case SWITCH:
                for (List<WorkflowTask> workflowTasks : children()) {
                    Iterator<WorkflowTask> iterator = workflowTasks.iterator();
                    while (iterator.hasNext()) {
                        WorkflowTask task = iterator.next();
                        if (task.getTaskReferenceName().equals(taskReferenceName)) {
                            break;
                        }
                        WorkflowTask nextTask = task.next(taskReferenceName, this);
                        if (nextTask != null) {
                            return nextTask;
                        }
                        if (task.has(taskReferenceName)) {
                            break;
                        }
                    }
                    if (iterator.hasNext()) {
                        return iterator.next();
                    }
                }
                if (taskType == TaskType.DO_WHILE && this.has(taskReferenceName)) {
                    // come here means this is DO_WHILE task and `taskReferenceName` is the last
                    // task in
                    // this DO_WHILE task, because DO_WHILE task need to be executed to decide
                    // whether to
                    // schedule next iteration, so we just return the DO_WHILE task, and then ignore
                    // generating this task again in deciderService.getNextTask()
                    return this;
                }
                break;
            case FORK_JOIN:
                boolean found = false;
                for (List<WorkflowTask> workflowTasks : children()) {
                    Iterator<WorkflowTask> iterator = workflowTasks.iterator();
                    while (iterator.hasNext()) {
                        WorkflowTask task = iterator.next();
                        if (task.getTaskReferenceName().equals(taskReferenceName)) {
                            found = true;
                            break;
                        }
                        WorkflowTask nextTask = task.next(taskReferenceName, this);
                        if (nextTask != null) {
                            return nextTask;
                        }
                        if (task.has(taskReferenceName)) {
                            break;
                        }
                    }
                    if (iterator.hasNext()) {
                        return iterator.next();
                    }
                    if (found && parent != null) {
                        return parent.next(
                                this.taskReferenceName,
                                parent); // we need to return join task... -- get my sibling from my
                        // parent..
                    }
                }
                break;
            case DYNAMIC:
            case TERMINATE:
            case SIMPLE:
                return null;
            default:
                break;
        }
        return null;
    }

    public boolean has(String taskReferenceName) {
        if (this.getTaskReferenceName().equals(taskReferenceName)) {
            return true;
        }

        switch (TaskType.of(type)) {
            case DECISION:
            case SWITCH:
            case DO_WHILE:
            case FORK_JOIN:
                for (List<WorkflowTask> childx : children()) {
                    for (WorkflowTask child : childx) {
                        if (child.has(taskReferenceName)) {
                            return true;
                        }
                    }
                }
                break;
            default:
                break;
        }
        return false;
    }

    public WorkflowTask get(String taskReferenceName) {

        if (this.getTaskReferenceName().equals(taskReferenceName)) {
            return this;
        }
        for (List<WorkflowTask> childx : children()) {
            for (WorkflowTask child : childx) {
                WorkflowTask found = child.get(taskReferenceName);
                if (found != null) {
                    return found;
                }
            }
        }
        return null;
    }

    @Override
    public String toString() {
        return name + "/" + taskReferenceName;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        WorkflowTask that = (WorkflowTask) o;
        return getStartDelay() == that.getStartDelay()
                && isOptional() == that.isOptional()
                && Objects.equals(getName(), that.getName())
                && Objects.equals(getTaskReferenceName(), that.getTaskReferenceName())
                && Objects.equals(getDescription(), that.getDescription())
                && Objects.equals(getInputParameters(), that.getInputParameters())
                && Objects.equals(getType(), that.getType())
                && Objects.equals(getDynamicTaskNameParam(), that.getDynamicTaskNameParam())
                && Objects.equals(getCaseValueParam(), that.getCaseValueParam())
                && Objects.equals(getEvaluatorType(), that.getEvaluatorType())
                && Objects.equals(getExpression(), that.getExpression())
                && Objects.equals(getCaseExpression(), that.getCaseExpression())
                && Objects.equals(getDecisionCases(), that.getDecisionCases())
                && Objects.equals(
                        getDynamicForkJoinTasksParam(), that.getDynamicForkJoinTasksParam())
                && Objects.equals(getDynamicForkTasksParam(), that.getDynamicForkTasksParam())
                && Objects.equals(
                        getDynamicForkTasksInputParamName(),
                        that.getDynamicForkTasksInputParamName())
                && Objects.equals(getDefaultCase(), that.getDefaultCase())
                && Objects.equals(getForkTasks(), that.getForkTasks())
                && Objects.equals(getSubWorkflowParam(), that.getSubWorkflowParam())
                && Objects.equals(getJoinOn(), that.getJoinOn())
                && Objects.equals(getSink(), that.getSink())
                && Objects.equals(isAsyncComplete(), that.isAsyncComplete())
                && Objects.equals(getDefaultExclusiveJoinTask(), that.getDefaultExclusiveJoinTask())
                && Objects.equals(getRetryCount(), that.getRetryCount());
    }

    @Override
    public int hashCode() {

        return Objects.hash(
                getName(),
                getTaskReferenceName(),
                getDescription(),
                getInputParameters(),
                getType(),
                getDynamicTaskNameParam(),
                getCaseValueParam(),
                getCaseExpression(),
                getEvaluatorType(),
                getExpression(),
                getDecisionCases(),
                getDynamicForkJoinTasksParam(),
                getDynamicForkTasksParam(),
                getDynamicForkTasksInputParamName(),
                getDefaultCase(),
                getForkTasks(),
                getStartDelay(),
                getSubWorkflowParam(),
                getJoinOn(),
                getSink(),
                isAsyncComplete(),
                isOptional(),
                getDefaultExclusiveJoinTask(),
                getRetryCount());
    }
}

core/src/main/java/com/netflix/conductor/core/utils/ParametersUtils.java
@Component
public class ParametersUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(ParametersUtils.class);
    private static final Pattern PATTERN =
            Pattern.compile(
                    "(?=(?<!\\$)\\$\\{)(?:(?=.*?\\{(?!.*?\\1)(.*\\}(?!.*\\2).*))(?=.*?\\}(?!.*?\\2)(.*)).)+?.*?(?=\\1)[^{]*(?=\\2$)",
                    Pattern.DOTALL);

    private final ObjectMapper objectMapper;
    private final TypeReference<Map<String, Object>> map = new TypeReference<>() {};

    public ParametersUtils(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    public Map<String, Object> getTaskInput(
            Map<String, Object> inputParams,
            WorkflowModel workflow,
            TaskDef taskDefinition,
            String taskId) {
        if (workflow.getWorkflowDefinition().getSchemaVersion() > 1) {
            return getTaskInputV2(inputParams, workflow, taskId, taskDefinition);
        }
        return getTaskInputV1(workflow, inputParams);
    }

    public Map<String, Object> getTaskInputV2(
            Map<String, Object> input,
            WorkflowModel workflow,
            String taskId,
            TaskDef taskDefinition) {
        Map<String, Object> inputParams;

        if (input != null) {
            inputParams = clone(input);
        } else {
            inputParams = new HashMap<>();
        }
        if (taskDefinition != null && taskDefinition.getInputTemplate() != null) {
            clone(taskDefinition.getInputTemplate()).forEach(inputParams::putIfAbsent);
        }

        Map<String, Map<String, Object>> inputMap = new HashMap<>();

        Map<String, Object> workflowParams = new HashMap<>();
        workflowParams.put("input", workflow.getInput());
        workflowParams.put("output", workflow.getOutput());
        workflowParams.put("status", workflow.getStatus());
        workflowParams.put("workflowId", workflow.getWorkflowId());
        workflowParams.put("parentWorkflowId", workflow.getParentWorkflowId());
        workflowParams.put("parentWorkflowTaskId", workflow.getParentWorkflowTaskId());
        workflowParams.put("workflowType", workflow.getWorkflowName());
        workflowParams.put("version", workflow.getWorkflowVersion());
        workflowParams.put("correlationId", workflow.getCorrelationId());
        workflowParams.put("reasonForIncompletion", workflow.getReasonForIncompletion());
        workflowParams.put("schemaVersion", workflow.getWorkflowDefinition().getSchemaVersion());
        workflowParams.put("variables", workflow.getVariables());

        inputMap.put("workflow", workflowParams);

        // For new workflow being started the list of tasks will be empty
        workflow.getTasks().stream()
                .map(TaskModel::getReferenceTaskName)
                .map(workflow::getTaskByRefName)
                .forEach(
                        task -> {
                            Map<String, Object> taskParams = new HashMap<>();
                            taskParams.put("input", task.getInputData());
                            taskParams.put("output", task.getOutputData());
                            taskParams.put("taskType", task.getTaskType());
                            if (task.getStatus() != null) {
                                taskParams.put("status", task.getStatus().toString());
                            }
                            taskParams.put("referenceTaskName", task.getReferenceTaskName());
                            taskParams.put("retryCount", task.getRetryCount());
                            taskParams.put("correlationId", task.getCorrelationId());
                            taskParams.put("pollCount", task.getPollCount());
                            taskParams.put("taskDefName", task.getTaskDefName());
                            taskParams.put("scheduledTime", task.getScheduledTime());
                            taskParams.put("startTime", task.getStartTime());
                            taskParams.put("endTime", task.getEndTime());
                            taskParams.put("workflowInstanceId", task.getWorkflowInstanceId());
                            taskParams.put("taskId", task.getTaskId());
                            taskParams.put(
                                    "reasonForIncompletion", task.getReasonForIncompletion());
                            taskParams.put("callbackAfterSeconds", task.getCallbackAfterSeconds());
                            taskParams.put("workerId", task.getWorkerId());
                            taskParams.put("iteration", task.getIteration());
                            inputMap.put(
                                    task.isLoopOverTask()
                                            ? TaskUtils.removeIterationFromTaskRefName(
                                                    task.getReferenceTaskName())
                                            : task.getReferenceTaskName(),
                                    taskParams);
                        });

        Configuration option =
                Configuration.defaultConfiguration().addOptions(Option.SUPPRESS_EXCEPTIONS);
        DocumentContext documentContext = JsonPath.parse(inputMap, option);
        Map<String, Object> replacedTaskInput = replace(inputParams, documentContext, taskId);
        if (taskDefinition != null && taskDefinition.getInputTemplate() != null) {
            // If input for a given key resolves to null, try replacing it with one from
            // inputTemplate, if it exists.
            replacedTaskInput.replaceAll(
                    (key, value) ->
                            (value == null) ? taskDefinition.getInputTemplate().get(key) : value);
        }
        return replacedTaskInput;
    }

    // deep clone using json - POJO
    private Map<String, Object> clone(Map<String, Object> inputTemplate) {
        try {
            byte[] bytes = objectMapper.writeValueAsBytes(inputTemplate);
            return objectMapper.readValue(bytes, map);
        } catch (IOException e) {
            throw new RuntimeException("Unable to clone input params", e);
        }
    }

    public Map<String, Object> replace(Map<String, Object> input, Object json) {
        Object doc;
        if (json instanceof String) {
            doc = JsonPath.parse(json.toString());
        } else {
            doc = json;
        }
        Configuration option =
                Configuration.defaultConfiguration().addOptions(Option.SUPPRESS_EXCEPTIONS);
        DocumentContext documentContext = JsonPath.parse(doc, option);
        return replace(input, documentContext, null);
    }

    public Object replace(String paramString) {
        Configuration option =
                Configuration.defaultConfiguration().addOptions(Option.SUPPRESS_EXCEPTIONS);
        DocumentContext documentContext = JsonPath.parse(Collections.emptyMap(), option);
        return replaceVariables(paramString, documentContext, null);
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> replace(
            Map<String, Object> input, DocumentContext documentContext, String taskId) {
        Map<String, Object> result = new HashMap<>();
        for (Entry<String, Object> e : input.entrySet()) {
            Object newValue;
            Object value = e.getValue();
            if (value instanceof String) {
                newValue = replaceVariables(value.toString(), documentContext, taskId);
            } else if (value instanceof Map) {
                // recursive call
                newValue = replace((Map<String, Object>) value, documentContext, taskId);
            } else if (value instanceof List) {
                newValue = replaceList((List<?>) value, taskId, documentContext);
            } else {
                newValue = value;
            }
            result.put(e.getKey(), newValue);
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    private Object replaceList(List<?> values, String taskId, DocumentContext io) {
        List<Object> replacedList = new LinkedList<>();
        for (Object listVal : values) {
            if (listVal instanceof String) {
                Object replaced = replaceVariables(listVal.toString(), io, taskId);
                replacedList.add(replaced);
            } else if (listVal instanceof Map) {
                Object replaced = replace((Map<String, Object>) listVal, io, taskId);
                replacedList.add(replaced);
            } else if (listVal instanceof List) {
                Object replaced = replaceList((List<?>) listVal, taskId, io);
                replacedList.add(replaced);
            } else {
                replacedList.add(listVal);
            }
        }
        return replacedList;
    }

    private Object replaceVariables(
            String paramString, DocumentContext documentContext, String taskId) {
        return replaceVariables(paramString, documentContext, taskId, 0);
    }

    private Object replaceVariables(
            String paramString, DocumentContext documentContext, String taskId, int depth) {
        var matcher = PATTERN.matcher(paramString);
        var replacements = new LinkedList<Replacement>();
        while (matcher.find()) {
            var start = matcher.start();
            var end = matcher.end();
            var match = paramString.substring(start, end);
            String paramPath = match.substring(2, match.length() - 1);
            paramPath = replaceVariables(paramPath, documentContext, taskId, depth + 1).toString();
            // if the paramPath is blank, meaning no value in between ${ and }
            // like ${}, ${  } etc, set the value to empty string
            if (StringUtils.isBlank(paramPath)) {
                replacements.add(new Replacement("", start, end));
                continue;
            }
            if (EnvUtils.isEnvironmentVariable(paramPath)) {
                String sysValue = EnvUtils.getSystemParametersValue(paramPath, taskId);
                if (sysValue != null) {
                    replacements.add(new Replacement(sysValue, start, end));
                }
            } else {
                try {
                    replacements.add(new Replacement(documentContext.read(paramPath), start, end));
                } catch (Exception e) {
                    LOGGER.warn(
                            "Error reading documentContext for paramPath: {}. Exception: {}",
                            paramPath,
                            e);
                    replacements.add(new Replacement(null, start, end));
                }
            }
        }
        if (replacements.size() == 1
                && replacements.getFirst().getStartIndex() == 0
                && replacements.getFirst().getEndIndex() == paramString.length()
                && depth == 0) {
            return replacements.get(0).getReplacement();
        }
        Collections.sort(replacements);
        var builder = new StringBuilder(paramString);
        for (int i = replacements.size() - 1; i >= 0; i--) {
            var replacement = replacements.get(i);
            builder.replace(
                    replacement.getStartIndex(),
                    replacement.getEndIndex(),
                    Objects.toString(replacement.getReplacement()));
        }
        return builder.toString().replaceAll("\\$\\$\\{", "\\${");
    }

    @Deprecated
    // Workflow schema version 1 is deprecated and new workflows should be using version 2
    private Map<String, Object> getTaskInputV1(
            WorkflowModel workflow, Map<String, Object> inputParams) {
        Map<String, Object> input = new HashMap<>();
        if (inputParams == null) {
            return input;
        }
        Map<String, Object> workflowInput = workflow.getInput();
        inputParams.forEach(
                (paramName, value) -> {
                    String paramPath = "" + value;
                    String[] paramPathComponents = paramPath.split("\\.");
                    Utils.checkArgument(
                            paramPathComponents.length == 3,
                            "Invalid input expression for "
                                    + paramName
                                    + ", paramPathComponents.size="
                                    + paramPathComponents.length
                                    + ", expression="
                                    + paramPath);

                    String source = paramPathComponents[0]; // workflow, or task reference name
                    String type = paramPathComponents[1]; // input/output
                    String name = paramPathComponents[2]; // name of the parameter
                    if ("workflow".equals(source)) {
                        input.put(paramName, workflowInput.get(name));
                    } else {
                        TaskModel task = workflow.getTaskByRefName(source);
                        if (task != null) {
                            if ("input".equals(type)) {
                                input.put(paramName, task.getInputData().get(name));
                            } else {
                                input.put(paramName, task.getOutputData().get(name));
                            }
                        }
                    }
                });
        return input;
    }

    public Map<String, Object> getWorkflowInput(
            WorkflowDef workflowDef, Map<String, Object> inputParams) {
        if (workflowDef != null && workflowDef.getInputTemplate() != null) {
            clone(workflowDef.getInputTemplate()).forEach(inputParams::putIfAbsent);
        }
        return inputParams;
    }

    private static class Replacement implements Comparable<Replacement> {
        private final int startIndex;
        private final int endIndex;
        private final Object replacement;

        public Replacement(Object replacement, int startIndex, int endIndex) {
            this.replacement = replacement;
            this.startIndex = startIndex;
            this.endIndex = endIndex;
        }

        public Object getReplacement() {
            return replacement;
        }

        public int getStartIndex() {
            return startIndex;
        }

        public int getEndIndex() {
            return endIndex;
        }

        @Override
        public int compareTo(Replacement o) {
            return Long.compare(startIndex, o.startIndex);
        }
    }
}

common/src/main/java/com/netflix/conductor/common/metadata/tasks/TaskType.java
@ProtoEnum
public enum TaskType {
    SIMPLE,
    DYNAMIC,
    FORK_JOIN,
    FORK_JOIN_DYNAMIC,
    DECISION,
    SWITCH,
    JOIN,
    DO_WHILE,
    SUB_WORKFLOW,
    START_WORKFLOW,
    EVENT,
    WAIT,
    HUMAN,
    USER_DEFINED,
    HTTP,
    LAMBDA,
    INLINE,
    EXCLUSIVE_JOIN,
    TERMINATE,
    KAFKA_PUBLISH,
    JSON_JQ_TRANSFORM,
    SET_VARIABLE,
    NOOP;

    /**
     * TaskType constants representing each of the possible enumeration values. Motivation: to not
     * have any hardcoded/inline strings used in the code.
     */
    public static final String TASK_TYPE_DECISION = "DECISION";

    public static final String TASK_TYPE_SWITCH = "SWITCH";
    public static final String TASK_TYPE_DYNAMIC = "DYNAMIC";
    public static final String TASK_TYPE_JOIN = "JOIN";
    public static final String TASK_TYPE_DO_WHILE = "DO_WHILE";
    public static final String TASK_TYPE_FORK_JOIN_DYNAMIC = "FORK_JOIN_DYNAMIC";
    public static final String TASK_TYPE_EVENT = "EVENT";
    public static final String TASK_TYPE_WAIT = "WAIT";
    public static final String TASK_TYPE_HUMAN = "HUMAN";
    public static final String TASK_TYPE_SUB_WORKFLOW = "SUB_WORKFLOW";
    public static final String TASK_TYPE_START_WORKFLOW = "START_WORKFLOW";
    public static final String TASK_TYPE_FORK_JOIN = "FORK_JOIN";
    public static final String TASK_TYPE_SIMPLE = "SIMPLE";
    public static final String TASK_TYPE_HTTP = "HTTP";
    public static final String TASK_TYPE_LAMBDA = "LAMBDA";
    public static final String TASK_TYPE_INLINE = "INLINE";
    public static final String TASK_TYPE_EXCLUSIVE_JOIN = "EXCLUSIVE_JOIN";
    public static final String TASK_TYPE_TERMINATE = "TERMINATE";
    public static final String TASK_TYPE_KAFKA_PUBLISH = "KAFKA_PUBLISH";
    public static final String TASK_TYPE_JSON_JQ_TRANSFORM = "JSON_JQ_TRANSFORM";
    public static final String TASK_TYPE_SET_VARIABLE = "SET_VARIABLE";
    public static final String TASK_TYPE_FORK = "FORK";
    public static final String TASK_TYPE_NOOP = "NOOP";

    private static final Set<String> BUILT_IN_TASKS = new HashSet<>();

    static {
        BUILT_IN_TASKS.add(TASK_TYPE_DECISION);
        BUILT_IN_TASKS.add(TASK_TYPE_SWITCH);
        BUILT_IN_TASKS.add(TASK_TYPE_FORK);
        BUILT_IN_TASKS.add(TASK_TYPE_JOIN);
        BUILT_IN_TASKS.add(TASK_TYPE_EXCLUSIVE_JOIN);
        BUILT_IN_TASKS.add(TASK_TYPE_DO_WHILE);
    }

    /**
     * Converts a task type string to {@link TaskType}. For an unknown string, the value is
     * defaulted to {@link TaskType#USER_DEFINED}.
     *
     *
NOTE: Use {@link Enum#valueOf(Class, String)} if the default of USER_DEFINED is not
     * necessary.
     *
     * @param taskType The task type string.
     * @return The {@link TaskType} enum.
     */
    public static TaskType of(String taskType) {
        try {
            return TaskType.valueOf(taskType);
        } catch (IllegalArgumentException iae) {
            return TaskType.USER_DEFINED;
        }
    }

    public static boolean isBuiltIn(String taskType) {
        return BUILT_IN_TASKS.contains(taskType);
    }
}

core/src/main/java/com/netflix/conductor/model/TaskModel.java
public class TaskModel {

    public enum Status {
        IN_PROGRESS(false, true, true),
        CANCELED(true, false, false),
        FAILED(true, false, true),
        FAILED_WITH_TERMINAL_ERROR(true, false, false),
        COMPLETED(true, true, true),
        COMPLETED_WITH_ERRORS(true, true, true),
        SCHEDULED(false, true, true),
        TIMED_OUT(true, false, true),
        SKIPPED(true, true, false);

        private final boolean terminal;

        private final boolean successful;

        private final boolean retriable;

        Status(boolean terminal, boolean successful, boolean retriable) {
            this.terminal = terminal;
            this.successful = successful;
            this.retriable = retriable;
        }

        public boolean isTerminal() {
            return terminal;
        }

        public boolean isSuccessful() {
            return successful;
        }

        public boolean isRetriable() {
            return retriable;
        }
    }

    private String taskType;

    private Status status;

    private String referenceTaskName;

    private int retryCount;

    private int seq;

    private String correlationId;

    private int pollCount;

    private String taskDefName;

    /** Time when the task was scheduled */
    private long scheduledTime;

    /** Time when the task was first polled */
    private long startTime;

    /** Time when the task completed executing */
    private long endTime;

    /** Time when the task was last updated */
    private long updateTime;

    private int startDelayInSeconds;

    private String retriedTaskId;

    private boolean retried;

    private boolean executed;

    private boolean callbackFromWorker = true;

    private long responseTimeoutSeconds;

    private String workflowInstanceId;

    private String workflowType;

    private String taskId;

    private String reasonForIncompletion;

    private long callbackAfterSeconds;

    private String workerId;

    private WorkflowTask workflowTask;

    private String domain;

    private Any inputMessage;

    private Any outputMessage;

    private int rateLimitPerFrequency;

    private int rateLimitFrequencyInSeconds;

    private String externalInputPayloadStoragePath;

    private String externalOutputPayloadStoragePath;

    private int workflowPriority;

    private String executionNameSpace;

    private String isolationGroupId;

    private int iteration;

    private String subWorkflowId;

    // Timeout after which the wait task should be marked as completed
    private long waitTimeout;

    /**
     * Used to note that a sub workflow associated with SUB_WORKFLOW task has an action performed on
     * it directly.
     */
    private boolean subworkflowChanged;

    @JsonIgnore private Map<String, Object> inputPayload = new HashMap<>();

    @JsonIgnore private Map<String, Object> outputPayload = new HashMap<>();

    @JsonIgnore private Map<String, Object> inputData = new HashMap<>();

    @JsonIgnore private Map<String, Object> outputData = new HashMap<>();

    public String getTaskType() {
        return taskType;
    }

    public void setTaskType(String taskType) {
        this.taskType = taskType;
    }

    public Status getStatus() {
        return status;
    }

    public void setStatus(Status status) {
        this.status = status;
    }

    @JsonIgnore
    public Map<String, Object> getInputData() {
        if (!inputPayload.isEmpty() && !inputData.isEmpty()) {
            inputData.putAll(inputPayload);
            inputPayload = new HashMap<>();
            return inputData;
        } else if (inputPayload.isEmpty()) {
            return inputData;
        } else {
            return inputPayload;
        }
    }

    @JsonIgnore
    public void setInputData(Map<String, Object> inputData) {
        if (inputData == null) {
            inputData = new HashMap<>();
        }
        this.inputData = inputData;
    }

    /**
     * @deprecated Used only for JSON serialization and deserialization.
     */
    @JsonProperty("inputData")
    @Deprecated
    public void setRawInputData(Map<String, Object> inputData) {
        setInputData(inputData);
    }

    /**
     * @deprecated Used only for JSON serialization and deserialization.
     */
    @JsonProperty("inputData")
    @Deprecated
    public Map<String, Object> getRawInputData() {
        return inputData;
    }

    public String getReferenceTaskName() {
        return referenceTaskName;
    }

    public void setReferenceTaskName(String referenceTaskName) {
        this.referenceTaskName = referenceTaskName;
    }

    public int getRetryCount() {
        return retryCount;
    }

    public void setRetryCount(int retryCount) {
        this.retryCount = retryCount;
    }

    public int getSeq() {
        return seq;
    }

    public void setSeq(int seq) {
        this.seq = seq;
    }

    public String getCorrelationId() {
        return correlationId;
    }

    public void setCorrelationId(String correlationId) {
        this.correlationId = correlationId;
    }

    public int getPollCount() {
        return pollCount;
    }

    public void setPollCount(int pollCount) {
        this.pollCount = pollCount;
    }

    public String getTaskDefName() {
        if (taskDefName == null || "".equals(taskDefName)) {
            taskDefName = taskType;
        }
        return taskDefName;
    }

    public void setTaskDefName(String taskDefName) {
        this.taskDefName = taskDefName;
    }

    public long getScheduledTime() {
        return scheduledTime;
    }

    public void setScheduledTime(long scheduledTime) {
        this.scheduledTime = scheduledTime;
    }

    public long getStartTime() {
        return startTime;
    }

    public void setStartTime(long startTime) {
        this.startTime = startTime;
    }

    public long getEndTime() {
        return endTime;
    }

    public void setEndTime(long endTime) {
        this.endTime = endTime;
    }

    public long getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(long updateTime) {
        this.updateTime = updateTime;
    }

    public int getStartDelayInSeconds() {
        return startDelayInSeconds;
    }

    public void setStartDelayInSeconds(int startDelayInSeconds) {
        this.startDelayInSeconds = startDelayInSeconds;
    }

    public String getRetriedTaskId() {
        return retriedTaskId;
    }

    public void setRetriedTaskId(String retriedTaskId) {
        this.retriedTaskId = retriedTaskId;
    }

    public boolean isRetried() {
        return retried;
    }

    public void setRetried(boolean retried) {
        this.retried = retried;
    }

    public boolean isExecuted() {
        return executed;
    }

    public void setExecuted(boolean executed) {
        this.executed = executed;
    }

    public boolean isCallbackFromWorker() {
        return callbackFromWorker;
    }

    public void setCallbackFromWorker(boolean callbackFromWorker) {
        this.callbackFromWorker = callbackFromWorker;
    }

    public long getResponseTimeoutSeconds() {
        return responseTimeoutSeconds;
    }

    public void setResponseTimeoutSeconds(long responseTimeoutSeconds) {
        this.responseTimeoutSeconds = responseTimeoutSeconds;
    }

    public String getWorkflowInstanceId() {
        return workflowInstanceId;
    }

    public void setWorkflowInstanceId(String workflowInstanceId) {
        this.workflowInstanceId = workflowInstanceId;
    }

    public String getWorkflowType() {
        return workflowType;
    }

    public void setWorkflowType(String workflowType) {
        this.workflowType = workflowType;
    }

    public String getTaskId() {
        return taskId;
    }

    public void setTaskId(String taskId) {
        this.taskId = taskId;
    }

    public String getReasonForIncompletion() {
        return reasonForIncompletion;
    }

    public void setReasonForIncompletion(String reasonForIncompletion) {
        this.reasonForIncompletion = reasonForIncompletion;
    }

    public long getCallbackAfterSeconds() {
        return callbackAfterSeconds;
    }

    public void setCallbackAfterSeconds(long callbackAfterSeconds) {
        this.callbackAfterSeconds = callbackAfterSeconds;
    }

    public String getWorkerId() {
        return workerId;
    }

    public void setWorkerId(String workerId) {
        this.workerId = workerId;
    }

    @JsonIgnore
    public Map<String, Object> getOutputData() {
        if (!outputPayload.isEmpty() && !outputData.isEmpty()) {
            // Combine payload + data
            // data has precedence over payload because:
            //  with external storage enabled, payload contains the old values
            //  while data contains the latest and if payload took precedence, it
            //  would remove latest outputs
            outputPayload.forEach(outputData::putIfAbsent);
            outputPayload = new HashMap<>();
            return outputData;
        } else if (outputPayload.isEmpty()) {
            return outputData;
        } else {
            return outputPayload;
        }
    }

    @JsonIgnore
    public void setOutputData(Map<String, Object> outputData) {
        if (outputData == null) {
            outputData = new HashMap<>();
        }
        this.outputData = outputData;
    }

    /**
     * @deprecated Used only for JSON serialization and deserialization.
     */
    @JsonProperty("outputData")
    @Deprecated
    public void setRawOutputData(Map<String, Object> inputData) {
        setOutputData(inputData);
    }

    /**
     * @deprecated Used only for JSON serialization and deserialization.
     */
    @JsonProperty("outputData")
    @Deprecated
    public Map<String, Object> getRawOutputData() {
        return outputData;
    }

    public WorkflowTask getWorkflowTask() {
        return workflowTask;
    }

    public void setWorkflowTask(WorkflowTask workflowTask) {
        this.workflowTask = workflowTask;
    }

    public String getDomain() {
        return domain;
    }

    public void setDomain(String domain) {
        this.domain = domain;
    }

    public Any getInputMessage() {
        return inputMessage;
    }

    public void setInputMessage(Any inputMessage) {
        this.inputMessage = inputMessage;
    }

    public Any getOutputMessage() {
        return outputMessage;
    }

    public void setOutputMessage(Any outputMessage) {
        this.outputMessage = outputMessage;
    }

    public int getRateLimitPerFrequency() {
        return rateLimitPerFrequency;
    }

    public void setRateLimitPerFrequency(int rateLimitPerFrequency) {
        this.rateLimitPerFrequency = rateLimitPerFrequency;
    }

    public int getRateLimitFrequencyInSeconds() {
        return rateLimitFrequencyInSeconds;
    }

    public void setRateLimitFrequencyInSeconds(int rateLimitFrequencyInSeconds) {
        this.rateLimitFrequencyInSeconds = rateLimitFrequencyInSeconds;
    }

    public String getExternalInputPayloadStoragePath() {
        return externalInputPayloadStoragePath;
    }

    public void setExternalInputPayloadStoragePath(String externalInputPayloadStoragePath) {
        this.externalInputPayloadStoragePath = externalInputPayloadStoragePath;
    }

    public String getExternalOutputPayloadStoragePath() {
        return externalOutputPayloadStoragePath;
    }

    public void setExternalOutputPayloadStoragePath(String externalOutputPayloadStoragePath) {
        this.externalOutputPayloadStoragePath = externalOutputPayloadStoragePath;
    }

    public int getWorkflowPriority() {
        return workflowPriority;
    }

    public void setWorkflowPriority(int workflowPriority) {
        this.workflowPriority = workflowPriority;
    }

    public String getExecutionNameSpace() {
        return executionNameSpace;
    }

    public void setExecutionNameSpace(String executionNameSpace) {
        this.executionNameSpace = executionNameSpace;
    }

    public String getIsolationGroupId() {
        return isolationGroupId;
    }

    public void setIsolationGroupId(String isolationGroupId) {
        this.isolationGroupId = isolationGroupId;
    }

    public int getIteration() {
        return iteration;
    }

    public void setIteration(int iteration) {
        this.iteration = iteration;
    }

    public String getSubWorkflowId() {
        // For backwards compatibility
        if (StringUtils.isNotBlank(subWorkflowId)) {
            return subWorkflowId;
        } else {
            return this.getOutputData() != null && this.getOutputData().get("subWorkflowId") != null
                    ? (String) this.getOutputData().get("subWorkflowId")
                    : this.getInputData() != null
                            ? (String) this.getInputData().get("subWorkflowId")
                            : null;
        }
    }

    public void setSubWorkflowId(String subWorkflowId) {
        this.subWorkflowId = subWorkflowId;
        // For backwards compatibility
        if (this.outputData != null && this.outputData.containsKey("subWorkflowId")) {
            this.outputData.put("subWorkflowId", subWorkflowId);
        }
    }

    public boolean isSubworkflowChanged() {
        return subworkflowChanged;
    }

    public void setSubworkflowChanged(boolean subworkflowChanged) {
        this.subworkflowChanged = subworkflowChanged;
    }

    public void incrementPollCount() {
        ++this.pollCount;
    }

    /**
     * @return {@link Optional} containing the task definition if available
     */
    public Optional<TaskDef> getTaskDefinition() {
        return Optional.ofNullable(this.getWorkflowTask()).map(WorkflowTask::getTaskDefinition);
    }

    public boolean isLoopOverTask() {
        return iteration > 0;
    }

    public long getWaitTimeout() {
        return waitTimeout;
    }

    public void setWaitTimeout(long waitTimeout) {
        this.waitTimeout = waitTimeout;
    }

    /**
     * @return the queueWaitTime
     */
    public long getQueueWaitTime() {
        if (this.startTime > 0 && this.scheduledTime > 0) {
            if (this.updateTime > 0 && getCallbackAfterSeconds() > 0) {
                long waitTime =
                        System.currentTimeMillis()
                                - (this.updateTime + (getCallbackAfterSeconds() * 1000));
                return waitTime > 0 ? waitTime : 0;
            } else {
                return this.startTime - this.scheduledTime;
            }
        }
        return 0L;
    }

    /**
     * @return a copy of the task instance
     */
    public TaskModel copy() {
        TaskModel copy = new TaskModel();
        BeanUtils.copyProperties(this, copy);
        return copy;
    }

    public void externalizeInput(String path) {
        this.inputPayload = this.inputData;
        this.inputData = new HashMap<>();
        this.externalInputPayloadStoragePath = path;
    }

    public void externalizeOutput(String path) {
        this.outputPayload = this.outputData;
        this.outputData = new HashMap<>();
        this.externalOutputPayloadStoragePath = path;
    }

    public void internalizeInput(Map<String, Object> data) {
        this.inputData = new HashMap<>();
        this.inputPayload = data;
    }

    public void internalizeOutput(Map<String, Object> data) {
        this.outputData = new HashMap<>();
        this.outputPayload = data;
    }

    @Override
    public String toString() {
        return "TaskModel{"
                + "taskType='"
                + taskType
                + '\''
                + ", status="
                + status
                + ", inputData="
                + inputData
                + ", referenceTaskName='"
                + referenceTaskName
                + '\''
                + ", retryCount="
                + retryCount
                + ", seq="
                + seq
                + ", correlationId='"
                + correlationId
                + '\''
                + ", pollCount="
                + pollCount
                + ", taskDefName='"
                + taskDefName
                + '\''
                + ", scheduledTime="
                + scheduledTime
                + ", startTime="
                + startTime
                + ", endTime="
                + endTime
                + ", updateTime="
                + updateTime
                + ", startDelayInSeconds="
                + startDelayInSeconds
                + ", retriedTaskId='"
                + retriedTaskId
                + '\''
                + ", retried="
                + retried
                + ", executed="
                + executed
                + ", callbackFromWorker="
                + callbackFromWorker
                + ", responseTimeoutSeconds="
                + responseTimeoutSeconds
                + ", workflowInstanceId='"
                + workflowInstanceId
                + '\''
                + ", workflowType='"
                + workflowType
                + '\''
                + ", taskId='"
                + taskId
                + '\''
                + ", reasonForIncompletion='"
                + reasonForIncompletion
                + '\''
                + ", callbackAfterSeconds="
                + callbackAfterSeconds
                + ", workerId='"
                + workerId
                + '\''
                + ", outputData="
                + outputData
                + ", workflowTask="
                + workflowTask
                + ", domain='"
                + domain
                + '\''
                + ", waitTimeout='"
                + waitTimeout
                + '\''
                + ", inputMessage="
                + inputMessage
                + ", outputMessage="
                + outputMessage
                + ", rateLimitPerFrequency="
                + rateLimitPerFrequency
                + ", rateLimitFrequencyInSeconds="
                + rateLimitFrequencyInSeconds
                + ", externalInputPayloadStoragePath='"
                + externalInputPayloadStoragePath
                + '\''
                + ", externalOutputPayloadStoragePath='"
                + externalOutputPayloadStoragePath
                + '\''
                + ", workflowPriority="
                + workflowPriority
                + ", executionNameSpace='"
                + executionNameSpace
                + '\''
                + ", isolationGroupId='"
                + isolationGroupId
                + '\''
                + ", iteration="
                + iteration
                + ", subWorkflowId='"
                + subWorkflowId
                + '\''
                + ", subworkflowChanged="
                + subworkflowChanged
                + '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TaskModel taskModel = (TaskModel) o;
        return getRetryCount() == taskModel.getRetryCount()
                && getSeq() == taskModel.getSeq()
                && getPollCount() == taskModel.getPollCount()
                && getScheduledTime() == taskModel.getScheduledTime()
                && getStartTime() == taskModel.getStartTime()
                && getEndTime() == taskModel.getEndTime()
                && getUpdateTime() == taskModel.getUpdateTime()
                && getStartDelayInSeconds() == taskModel.getStartDelayInSeconds()
                && isRetried() == taskModel.isRetried()
                && isExecuted() == taskModel.isExecuted()
                && isCallbackFromWorker() == taskModel.isCallbackFromWorker()
                && getResponseTimeoutSeconds() == taskModel.getResponseTimeoutSeconds()
                && getCallbackAfterSeconds() == taskModel.getCallbackAfterSeconds()
                && getRateLimitPerFrequency() == taskModel.getRateLimitPerFrequency()
                && getRateLimitFrequencyInSeconds() == taskModel.getRateLimitFrequencyInSeconds()
                && getWorkflowPriority() == taskModel.getWorkflowPriority()
                && getIteration() == taskModel.getIteration()
                && isSubworkflowChanged() == taskModel.isSubworkflowChanged()
                && Objects.equals(getTaskType(), taskModel.getTaskType())
                && getStatus() == taskModel.getStatus()
                && Objects.equals(getInputData(), taskModel.getInputData())
                && Objects.equals(getReferenceTaskName(), taskModel.getReferenceTaskName())
                && Objects.equals(getCorrelationId(), taskModel.getCorrelationId())
                && Objects.equals(getTaskDefName(), taskModel.getTaskDefName())
                && Objects.equals(getRetriedTaskId(), taskModel.getRetriedTaskId())
                && Objects.equals(getWorkflowInstanceId(), taskModel.getWorkflowInstanceId())
                && Objects.equals(getWorkflowType(), taskModel.getWorkflowType())
                && Objects.equals(getTaskId(), taskModel.getTaskId())
                && Objects.equals(getReasonForIncompletion(), taskModel.getReasonForIncompletion())
                && Objects.equals(getWorkerId(), taskModel.getWorkerId())
                && Objects.equals(getWaitTimeout(), taskModel.getWaitTimeout())
                && Objects.equals(outputData, taskModel.outputData)
                && Objects.equals(outputPayload, taskModel.outputPayload)
                && Objects.equals(getWorkflowTask(), taskModel.getWorkflowTask())
                && Objects.equals(getDomain(), taskModel.getDomain())
                && Objects.equals(getInputMessage(), taskModel.getInputMessage())
                && Objects.equals(getOutputMessage(), taskModel.getOutputMessage())
                && Objects.equals(
                        getExternalInputPayloadStoragePath(),
                        taskModel.getExternalInputPayloadStoragePath())
                && Objects.equals(
                        getExternalOutputPayloadStoragePath(),
                        taskModel.getExternalOutputPayloadStoragePath())
                && Objects.equals(getExecutionNameSpace(), taskModel.getExecutionNameSpace())
                && Objects.equals(getIsolationGroupId(), taskModel.getIsolationGroupId())
                && Objects.equals(getSubWorkflowId(), taskModel.getSubWorkflowId());
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                getTaskType(),
                getStatus(),
                getInputData(),
                getReferenceTaskName(),
                getRetryCount(),
                getSeq(),
                getCorrelationId(),
                getPollCount(),
                getTaskDefName(),
                getScheduledTime(),
                getStartTime(),
                getEndTime(),
                getUpdateTime(),
                getStartDelayInSeconds(),
                getRetriedTaskId(),
                isRetried(),
                isExecuted(),
                isCallbackFromWorker(),
                getResponseTimeoutSeconds(),
                getWorkflowInstanceId(),
                getWorkflowType(),
                getTaskId(),
                getReasonForIncompletion(),
                getCallbackAfterSeconds(),
                getWorkerId(),
                getWaitTimeout(),
                outputData,
                outputPayload,
                getWorkflowTask(),
                getDomain(),
                getInputMessage(),
                getOutputMessage(),
                getRateLimitPerFrequency(),
                getRateLimitFrequencyInSeconds(),
                getExternalInputPayloadStoragePath(),
                getExternalOutputPayloadStoragePath(),
                getWorkflowPriority(),
                getExecutionNameSpace(),
                getIsolationGroupId(),
                getIteration(),
                getSubWorkflowId(),
                isSubworkflowChanged());
    }

    public Task toTask() {
        Task task = new Task();
        BeanUtils.copyProperties(this, task);
        task.setStatus(Task.Status.valueOf(status.name()));

        // ensure that input/output is properly represented
        if (externalInputPayloadStoragePath != null) {
            task.setInputData(new HashMap<>());
        }
        if (externalOutputPayloadStoragePath != null) {
            task.setOutputData(new HashMap<>());
        }
        return task;
    }

    public static Task.Status mapToTaskStatus(TaskModel.Status status) {
        return Task.Status.valueOf(status.name());
    }

    public void addInput(String key, Object value) {
        this.inputData.put(key, value);
    }

    public void addInput(Map<String, Object> inputData) {
        if (inputData != null) {
            this.inputData.putAll(inputData);
        }
    }

    public void addOutput(String key, Object value) {
        this.outputData.put(key, value);
    }

    public void addOutput(Map<String, Object> outputData) {
        if (outputData != null) {
            this.outputData.putAll(outputData);
        }
    }

    public void clearOutput() {
        this.outputData.clear();
        this.outputPayload.clear();
        this.externalOutputPayloadStoragePath = null;
    }
}

core/src/main/java/com/netflix/conductor/model/WorkflowModel.java
public class WorkflowModel {

    public enum Status {
        RUNNING(false, false),
        COMPLETED(true, true),
        FAILED(true, false),
        TIMED_OUT(true, false),
        TERMINATED(true, false),
        PAUSED(false, true);

        private final boolean terminal;
        private final boolean successful;

        Status(boolean terminal, boolean successful) {
            this.terminal = terminal;
            this.successful = successful;
        }

        public boolean isTerminal() {
            return terminal;
        }

        public boolean isSuccessful() {
            return successful;
        }
    }

    private Status status = Status.RUNNING;

    private long endTime;

    private String workflowId;

    private String parentWorkflowId;

    private String parentWorkflowTaskId;

    private List<TaskModel> tasks = new LinkedList<>();

    private String correlationId;

    private String reRunFromWorkflowId;

    private String reasonForIncompletion;

    private String event;

    private Map<String, String> taskToDomain = new HashMap<>();

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private Set<String> failedReferenceTaskNames = new HashSet<>();

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private Set<String> failedTaskNames = new HashSet<>();

    private WorkflowDef workflowDefinition;

    private String externalInputPayloadStoragePath;

    private String externalOutputPayloadStoragePath;

    private int priority;

    private Map<String, Object> variables = new HashMap<>();

    private long lastRetriedTime;

    private String ownerApp;

    private Long createTime;

    private Long updatedTime;

    private String createdBy;

    private String updatedBy;

    // Capture the failed taskId if the workflow execution failed because of task failure
    private String failedTaskId;

    private Status previousStatus;

    @JsonIgnore private Map<String, Object> input = new HashMap<>();

    @JsonIgnore private Map<String, Object> output = new HashMap<>();

    @JsonIgnore private Map<String, Object> inputPayload = new HashMap<>();

    @JsonIgnore private Map<String, Object> outputPayload = new HashMap<>();

    public Status getPreviousStatus() {
        return previousStatus;
    }

    public void setPreviousStatus(Status status) {
        this.previousStatus = status;
    }

    public Status getStatus() {
        return status;
    }

    public void setStatus(Status status) {
        // update previous status if current status changed
        if (this.status != status) {
            setPreviousStatus(this.status);
        }
        this.status = status;
    }

    public long getEndTime() {
        return endTime;
    }

    public void setEndTime(long endTime) {
        this.endTime = endTime;
    }

    public String getWorkflowId() {
        return workflowId;
    }

    public void setWorkflowId(String workflowId) {
        this.workflowId = workflowId;
    }

    public String getParentWorkflowId() {
        return parentWorkflowId;
    }

    public void setParentWorkflowId(String parentWorkflowId) {
        this.parentWorkflowId = parentWorkflowId;
    }

    public String getParentWorkflowTaskId() {
        return parentWorkflowTaskId;
    }

    public void setParentWorkflowTaskId(String parentWorkflowTaskId) {
        this.parentWorkflowTaskId = parentWorkflowTaskId;
    }

    public List<TaskModel> getTasks() {
        return tasks;
    }

    public void setTasks(List<TaskModel> tasks) {
        this.tasks = tasks;
    }

    @JsonIgnore
    public Map<String, Object> getInput() {
        if (!inputPayload.isEmpty() && !input.isEmpty()) {
            input.putAll(inputPayload);
            inputPayload = new HashMap<>();
            return input;
        } else if (inputPayload.isEmpty()) {
            return input;
        } else {
            return inputPayload;
        }
    }

    @JsonIgnore
    public void setInput(Map<String, Object> input) {
        if (input == null) {
            input = new HashMap<>();
        }
        this.input = input;
    }

    @JsonIgnore
    public Map<String, Object> getOutput() {
        if (!outputPayload.isEmpty() && !output.isEmpty()) {
            output.putAll(outputPayload);
            outputPayload = new HashMap<>();
            return output;
        } else if (outputPayload.isEmpty()) {
            return output;
        } else {
            return outputPayload;
        }
    }

    @JsonIgnore
    public void setOutput(Map<String, Object> output) {
        if (output == null) {
            output = new HashMap<>();
        }
        this.output = output;
    }

    /**
     * @deprecated Used only for JSON serialization and deserialization.
     */
    @Deprecated
    @JsonProperty("input")
    public Map<String, Object> getRawInput() {
        return input;
    }

    /**
     * @deprecated Used only for JSON serialization and deserialization.
     */
    @Deprecated
    @JsonProperty("input")
    public void setRawInput(Map<String, Object> input) {
        setInput(input);
    }

    /**
     * @deprecated Used only for JSON serialization and deserialization.
     */
    @Deprecated
    @JsonProperty("output")
    public Map<String, Object> getRawOutput() {
        return output;
    }

    /**
     * @deprecated Used only for JSON serialization and deserialization.
     */
    @Deprecated
    @JsonProperty("output")
    public void setRawOutput(Map<String, Object> output) {
        setOutput(output);
    }

    public String getCorrelationId() {
        return correlationId;
    }

    public void setCorrelationId(String correlationId) {
        this.correlationId = correlationId;
    }

    public String getReRunFromWorkflowId() {
        return reRunFromWorkflowId;
    }

    public void setReRunFromWorkflowId(String reRunFromWorkflowId) {
        this.reRunFromWorkflowId = reRunFromWorkflowId;
    }

    public String getReasonForIncompletion() {
        return reasonForIncompletion;
    }

    public void setReasonForIncompletion(String reasonForIncompletion) {
        this.reasonForIncompletion = reasonForIncompletion;
    }

    public String getEvent() {
        return event;
    }

    public void setEvent(String event) {
        this.event = event;
    }

    public Map<String, String> getTaskToDomain() {
        return taskToDomain;
    }

    public void setTaskToDomain(Map<String, String> taskToDomain) {
        this.taskToDomain = taskToDomain;
    }

    public Set<String> getFailedReferenceTaskNames() {
        return failedReferenceTaskNames;
    }

    public void setFailedReferenceTaskNames(Set<String> failedReferenceTaskNames) {
        this.failedReferenceTaskNames = failedReferenceTaskNames;
    }

    public Set<String> getFailedTaskNames() {
        return failedTaskNames;
    }

    public void setFailedTaskNames(Set<String> failedTaskNames) {
        this.failedTaskNames = failedTaskNames;
    }

    public WorkflowDef getWorkflowDefinition() {
        return workflowDefinition;
    }

    public void setWorkflowDefinition(WorkflowDef workflowDefinition) {
        this.workflowDefinition = workflowDefinition;
    }

    public String getExternalInputPayloadStoragePath() {
        return externalInputPayloadStoragePath;
    }

    public void setExternalInputPayloadStoragePath(String externalInputPayloadStoragePath) {
        this.externalInputPayloadStoragePath = externalInputPayloadStoragePath;
    }

    public String getExternalOutputPayloadStoragePath() {
        return externalOutputPayloadStoragePath;
    }

    public void setExternalOutputPayloadStoragePath(String externalOutputPayloadStoragePath) {
        this.externalOutputPayloadStoragePath = externalOutputPayloadStoragePath;
    }

    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        if (priority < 0 || priority > 99) {
            throw new IllegalArgumentException("priority MUST be between 0 and 99 (inclusive)");
        }
        this.priority = priority;
    }

    public Map<String, Object> getVariables() {
        return variables;
    }

    public void setVariables(Map<String, Object> variables) {
        this.variables = variables;
    }

    public long getLastRetriedTime() {
        return lastRetriedTime;
    }

    public void setLastRetriedTime(long lastRetriedTime) {
        this.lastRetriedTime = lastRetriedTime;
    }

    public String getOwnerApp() {
        return ownerApp;
    }

    public void setOwnerApp(String ownerApp) {
        this.ownerApp = ownerApp;
    }

    public Long getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Long createTime) {
        this.createTime = createTime;
    }

    public Long getUpdatedTime() {
        return updatedTime;
    }

    public void setUpdatedTime(Long updatedTime) {
        this.updatedTime = updatedTime;
    }

    public String getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

    public String getUpdatedBy() {
        return updatedBy;
    }

    public void setUpdatedBy(String updatedBy) {
        this.updatedBy = updatedBy;
    }

    public String getFailedTaskId() {
        return failedTaskId;
    }

    public void setFailedTaskId(String failedTaskId) {
        this.failedTaskId = failedTaskId;
    }

    /**
     * Convenience method for accessing the workflow definition name.
     *
     * @return the workflow definition name.
     */
    public String getWorkflowName() {
        Utils.checkNotNull(workflowDefinition, "Workflow definition is null");
        return workflowDefinition.getName();
    }

    /**
     * Convenience method for accessing the workflow definition version.
     *
     * @return the workflow definition version.
     */
    public int getWorkflowVersion() {
        Utils.checkNotNull(workflowDefinition, "Workflow definition is null");
        return workflowDefinition.getVersion();
    }

    public boolean hasParent() {
        return StringUtils.isNotEmpty(parentWorkflowId);
    }

    /**
     * A string representation of all relevant fields that identify this workflow. Intended for use
     * in log and other system generated messages.
     */
    public String toShortString() {
        String name = workflowDefinition != null ? workflowDefinition.getName() : null;
        Integer version = workflowDefinition != null ? workflowDefinition.getVersion() : null;
        return String.format("%s.%s/%s", name, version, workflowId);
    }

    public TaskModel getTaskByRefName(String refName) {
        if (refName == null) {
            throw new RuntimeException(
                    "refName passed is null.  Check the workflow execution.  For dynamic tasks, make sure referenceTaskName is set to a not null value");
        }
        LinkedList<TaskModel> found = new LinkedList<>();
        for (TaskModel task : tasks) {
            if (task.getReferenceTaskName() == null) {
                throw new RuntimeException(
                        "Task "
                                + task.getTaskDefName()
                                + ", seq="
                                + task.getSeq()
                                + " does not have reference name specified.");
            }
            if (task.getReferenceTaskName().equals(refName)) {
                found.add(task);
            }
        }
        if (found.isEmpty()) {
            return null;
        }
        return found.getLast();
    }

    public void externalizeInput(String path) {
        this.inputPayload = this.input;
        this.input = new HashMap<>();
        this.externalInputPayloadStoragePath = path;
    }

    public void externalizeOutput(String path) {
        this.outputPayload = this.output;
        this.output = new HashMap<>();
        this.externalOutputPayloadStoragePath = path;
    }

    public void internalizeInput(Map<String, Object> data) {
        this.input = new HashMap<>();
        this.inputPayload = data;
    }

    public void internalizeOutput(Map<String, Object> data) {
        this.output = new HashMap<>();
        this.outputPayload = data;
    }

    @Override
    public String toString() {
        String name = workflowDefinition != null ? workflowDefinition.getName() : null;
        Integer version = workflowDefinition != null ? workflowDefinition.getVersion() : null;
        return String.format("%s.%s/%s.%s", name, version, workflowId, status);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        WorkflowModel that = (WorkflowModel) o;
        return getEndTime() == that.getEndTime()
                && getPriority() == that.getPriority()
                && getLastRetriedTime() == that.getLastRetriedTime()
                && getStatus() == that.getStatus()
                && Objects.equals(getWorkflowId(), that.getWorkflowId())
                && Objects.equals(getParentWorkflowId(), that.getParentWorkflowId())
                && Objects.equals(getParentWorkflowTaskId(), that.getParentWorkflowTaskId())
                && Objects.equals(getTasks(), that.getTasks())
                && Objects.equals(getInput(), that.getInput())
                && Objects.equals(output, that.output)
                && Objects.equals(outputPayload, that.outputPayload)
                && Objects.equals(getCorrelationId(), that.getCorrelationId())
                && Objects.equals(getReRunFromWorkflowId(), that.getReRunFromWorkflowId())
                && Objects.equals(getReasonForIncompletion(), that.getReasonForIncompletion())
                && Objects.equals(getEvent(), that.getEvent())
                && Objects.equals(getTaskToDomain(), that.getTaskToDomain())
                && Objects.equals(getFailedReferenceTaskNames(), that.getFailedReferenceTaskNames())
                && Objects.equals(getFailedTaskNames(), that.getFailedTaskNames())
                && Objects.equals(getWorkflowDefinition(), that.getWorkflowDefinition())
                && Objects.equals(
                        getExternalInputPayloadStoragePath(),
                        that.getExternalInputPayloadStoragePath())
                && Objects.equals(
                        getExternalOutputPayloadStoragePath(),
                        that.getExternalOutputPayloadStoragePath())
                && Objects.equals(getVariables(), that.getVariables())
                && Objects.equals(getOwnerApp(), that.getOwnerApp())
                && Objects.equals(getCreateTime(), that.getCreateTime())
                && Objects.equals(getUpdatedTime(), that.getUpdatedTime())
                && Objects.equals(getCreatedBy(), that.getCreatedBy())
                && Objects.equals(getUpdatedBy(), that.getUpdatedBy());
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                getStatus(),
                getEndTime(),
                getWorkflowId(),
                getParentWorkflowId(),
                getParentWorkflowTaskId(),
                getTasks(),
                getInput(),
                output,
                outputPayload,
                getCorrelationId(),
                getReRunFromWorkflowId(),
                getReasonForIncompletion(),
                getEvent(),
                getTaskToDomain(),
                getFailedReferenceTaskNames(),
                getFailedTaskNames(),
                getWorkflowDefinition(),
                getExternalInputPayloadStoragePath(),
                getExternalOutputPayloadStoragePath(),
                getPriority(),
                getVariables(),
                getLastRetriedTime(),
                getOwnerApp(),
                getCreateTime(),
                getUpdatedTime(),
                getCreatedBy(),
                getUpdatedBy());
    }

    public Workflow toWorkflow() {
        Workflow workflow = new Workflow();
        BeanUtils.copyProperties(this, workflow);
        workflow.setStatus(Workflow.WorkflowStatus.valueOf(this.status.name()));
        workflow.setTasks(tasks.stream().map(TaskModel::toTask).collect(Collectors.toList()));
        workflow.setUpdateTime(this.updatedTime);

        // ensure that input/output is properly represented
        if (externalInputPayloadStoragePath != null) {
            workflow.setInput(new HashMap<>());
        }
        if (externalOutputPayloadStoragePath != null) {
            workflow.setOutput(new HashMap<>());
        }
        return workflow;
    }

    public void addInput(String key, Object value) {
        this.input.put(key, value);
    }

    public void addInput(Map<String, Object> inputData) {
        if (inputData != null) {
            this.input.putAll(inputData);
        }
    }

    public void addOutput(String key, Object value) {
        this.output.put(key, value);
    }

    public void addOutput(Map<String, Object> outputData) {
        if (outputData != null) {
            this.output.putAll(outputData);
        }
    }
}

Answer: return TaskType.SIMPLE.name();

<think>
The method getTaskType() should return the string identifier for the SIMPLE task type. In Conductor, task types are defined in TaskType enum, and the SIMPLE type is accessed via TaskType.SIMPLE.name(). Therefore the next line is the return statement for that value.
</think>
return TaskType.SIMPLE.name();