Workflows
In this chapter, you'll learn about workflows and how to define and execute them.
What is a Workflow?#
In digital commerce you typically have many systems involved in your operations. For example, you may have an ERP system that holds product master data and accounting reports, a CMS system for content, a CRM system for managing customer campaigns, a payment service to process credit cards, and so on. Sometimes you may even have custom built applications that need to participate in the commerce stack. One of the biggest challenges when operating a stack like this is ensuring consistency in the data spread across systems.
MedusaGo has a built-in durable execution engine to help complete tasks that span multiple systems. You orchestrate your operations across systems in MedusaGo instead of having to manage it yourself.
A workflow is a series of queries and actions, called steps, that complete a task. You write a workflow as a Go function that calls its steps.
However, unlike regular functions, workflows:
- Keep a journal of their steps, allowing you to track them and their progress.
- Support defining roll-back logic for each step, so that you can handle errors gracefully and your data remain consistent across systems.
- Perform long actions asynchronously, giving you control over when a step starts and finishes.
You implement all custom flows within workflows, then execute them from API routes, subscribers, and scheduled jobs.
transform and when.MedusaGo doesn't have those constraints. The body of a workflow is plain Go: a step runs when your code calls it, so you use variables, if statements, loops, and return as you do anywhere else in Go. So, the chapters about the constructor's constraints and about data manipulation don't exist in this documentation.How to Create and Execute a Workflow?#
1. Create the Steps#
A workflow is made of a series of steps. A step is created using composer.CreateStep from the Workflows SDK.
Create the file src/workflows/hello-world.go with the following content:
// step1 is a step with the unique name "step-1". It takes no input.
var step1 = composer.CreateStep("step-1",
func(ctx composer.StepExecutionContext, _ any) (composer.StepResponse[string], error) {
return composer.NewStepResponse("Hello from step one!"), nil
}, nil)
composer.CreateStep accepts the following parameters:
- The step's unique name,
step-1. - The step's function. It receives the step's context and its input, and returns the step's response and an error.
- The step's compensation function, which you'll learn about in the Compensation Function chapter. It's
nilif the step doesn't need one.
A step returns its output wrapped in a composer.StepResponse, which you create with composer.NewStepResponse. The type of the output is a type parameter of the step, so the workflow receives it with its type.
Steps can accept input parameters. For example, add the following to src/workflows/hello-world.go:
// WorkflowInput is the input of the workflow and of step2.
type WorkflowInput struct {
Name string `json:"name"`
}
// step2 receives the workflow's input.
var step2 = composer.CreateStep("step-2",
func(ctx composer.StepExecutionContext, input WorkflowInput) (composer.StepResponse[string], error) {
return composer.NewStepResponse(fmt.Sprintf("Hello %s from step two!", input.Name)), nil
}, nil)
This adds another step whose function receives a WorkflowInput value as its second parameter.
2. Create a Workflow#
Next, add the following to the same file to create the workflow using composer.CreateWorkflow:
// WorkflowOutput is what the workflow returns.
type WorkflowOutput struct {
Message string `json:"message"`
}
// HelloWorldWorkflow is the workflow with the unique name "hello-world".
var HelloWorldWorkflow = composer.CreateWorkflow("hello-world",
func(e *composer.Execution, input WorkflowInput) (WorkflowOutput, error) {
if _, err := step1.Run(e, nil); err != nil {
return WorkflowOutput{}, err
}
// to pass input
message, err := step2.Run(e, input)
if err != nil {
return WorkflowOutput{}, err
}
return WorkflowOutput{Message: message}, nil
})
composer.CreateWorkflow accepts the workflow's unique name as a first parameter, and the workflow's function as a second parameter. The type parameters of the workflow are its input and output types.
The workflow's function receives an *composer.Execution and the workflow's input. You run a step by calling its Run method with the execution and the step's input. It returns the step's output, or the error if the step failed. If a step fails, return its error from the workflow's function: the workflow then stops and compensates the steps that ran, as explained in the Error Handling chapter.
The value the function returns is returned to the workflow's executors.
3. Execute the Workflow#
You can execute a workflow from different customizations:
- Execute in an API route to expose the workflow's functionalities to clients.
- Execute in a subscriber to use the workflow's functionalities when a commerce operation is performed.
- Execute in a scheduled job to run the workflow's functionalities automatically at a specified repeated interval.
To execute the workflow, call its Run method with a context, the container, and the workflow's input:
Run returns the workflow's output and an error. The error is the first error a step returned, unchanged, so you can check it the way you check any Go error. In an API route, you return the error to let MedusaGo's error handler turn it into an HTTP response.
If you need the details of the execution's transaction, such as its ID, use RunWithInfo instead of Run. It returns the output, a composer.RunInfo value with the TransactionID, RunID, and State of the execution, and the error.
4. Test Workflow#
To test out your workflow, start your MedusaGo application:
Then, if you added the API route above, send a GET request to /workflow:
You'll receive the following response:
Access the Container in Workflow Steps#
A step receives a composer.StepExecutionContext as its first parameter. Its Container property is the container, which allows you to resolve Framework and commerce tools in your application. Its Context property is the context.Context to pass to the calls you make in the step.
For example, consider you want to implement a workflow that returns the total products in your application. Create the file src/workflows/product-count.go with the following content:
// getProductCountStep resolves the Product Module's service from the
// container, which is in the step's context.
var getProductCountStep = composer.CreateStep("get-product-count",
func(ctx composer.StepExecutionContext, _ any) (composer.StepResponse[int], error) {
productModuleService, ok := ctx.Container.Resolve(utilsmodulessdk.PRODUCT).(caller)
if !ok {
return composer.StepResponse[int]{}, errors.New("the Product Module isn't registered")
}
// listAndCountProducts returns the products and their total count
result, err := productModuleService.Call(ctx.Context, "listAndCountProducts", nil)
if err != nil {
return composer.StepResponse[int]{}, err
}
count := result.([]any)[1].(int)
return composer.NewStepResponse(count), nil
}, nil)
// ProductCountWorkflow returns the total number of products.
var ProductCountWorkflow = composer.CreateWorkflow("product-count",
func(e *composer.Execution, _ any) (ProductCountOutput, error) {
count, err := getProductCountStep.Run(e, nil)
if err != nil {
return ProductCountOutput{}, err
}
return ProductCountOutput{Count: count}, nil
})
In getProductCountStep, you use the container to resolve the Product Module's main service. Then, you call its generated listAndCountProducts method, which returns the products and their total count. You return the count in the step's response. You then call this step in the ProductCountWorkflow.
StepExecutionContext also has the name of the step and the workflow, the transaction ID, and Attempt, the number of the current attempt to execute the step (it's 1 unless the step is retried).Conditions#
Since the body of a workflow is Go code, you add conditions with if statements:
// ConditionalWorkflow only runs step2 if the input says so. The body is
// regular Go code, so a condition is an `if`.
var ConditionalWorkflow = composer.CreateWorkflow("conditional-hello",
func(e *composer.Execution, input ConditionalInput) (string, error) {
if !input.Greet {
return "", nil
}
return step2.Run(e, WorkflowInput{Name: input.Name})
})
If you want the condition to be part of the execution's journal, use composer.When. It accepts the execution, a name for the condition, the condition, and a function that runs the steps if the condition holds, and returns what the function returns:
// NamedConditionWorkflow records the condition as a step named
// "when-greet" in the execution's journal. step2 only runs if the condition
// holds.
var NamedConditionWorkflow = composer.CreateWorkflow("named-condition-hello",
func(e *composer.Execution, input ConditionalInput) (string, error) {
return composer.When(e, "when-greet", input.Greet, func() (string, error) {
return step2.Run(e, WorkflowInput{Name: input.Name})
})
})
Step Options#
A step's options configure how the workflow engine executes it. You set them by calling the Config method on the step, which returns a copy of the step with the options. Its first parameter is a new name for the step, or an empty string to keep its name. This way, you can use the same step with different options, or with different names, in the same workflow.
// retriedStep is retried twice if it fails.
var retriedStep = step1.Config("", composer.StepOptions{MaxRetries: 2})
The options you can set are:
MaxRetriesintContinueOnPermanentFailureboolNoCompensationboolAsyncboolBackgroundExecutionboolAsync.TimeoutintWhat's Next#
The next chapters explain the features of workflows in more detail:
- Compensation Function: undo what a step did when an error occurs.
- Error Handling: handle the errors of a workflow's steps.
- Workflow Hooks: consume the hooks of Medusa's workflows to inject your logic.
- Retry Failed Steps: retry a step if it fails.
- Run Workflow Steps in Parallel: group steps that don't rely on each other.
- Workflow Timeout: fail a step that waits for too long.
- Store Workflow Executions: keep the details of an execution in the database.
- Long-Running Workflows: run a workflow in the background and wait for external actions.
- Execute Nested Workflows: run a workflow as a step of another one.
- Locking Operations in Workflows: avoid race conditions.