Retry Failed Steps

In this chapter, you'll learn how to configure steps to allow retry on failure.

What is a Step Retry?#

A step retry is a mechanism that allows a step to be retried automatically when it fails. This is useful for handling transient errors, such as network issues or temporary unavailability of a service.

By default, when a step fails, the workflow execution stops, and the workflow is marked as failed. However, you can configure a step to retry on failure.

When a step fails, you can configure the workflow engine to automatically retry the step a specified number of times before marking the workflow as failed. This can help improve the reliability and resilience of your workflows.


Configure a Step's Retry#

By default, when an error occurs in a step, the step and the workflow fail, and the execution stops.

You can configure the step to retry on failure with the MaxRetries option. You set a step's options with its Config method, as explained in the Workflows chapter.

For example:

src/workflows/hello-world.go
// step1 is retried twice if it fails: it's executed three times at most.
var step1 = composer.CreateStep("step-1",
	func(ctx composer.StepExecutionContext, _ any) (composer.StepResponse[string], error) {
		slog.Info("Executing step 1", "attempt", ctx.Attempt)

		return composer.StepResponse[string]{}, common.NewMedusaError(
			common.MedusaErrorTypes.UNEXPECTED_STATE,
			"Oops! Something happened.",
		)
	}, nil).Config("", composer.StepOptions{MaxRetries: 2})

The MaxRetries option is a number indicating the number of times a step can be retried when it fails. The Attempt property of the step's context is the number of the current attempt, starting at 1.

When you execute a workflow that uses this step, you'll see the following result in the logs:

Terminal
INFO Executing step 1 attempt=1
INFO Executing step 1 attempt=2
INFO Executing step 1 attempt=3

The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail, and Run returns the error of the last attempt.

Note: A step that returns composer.PermanentFailure isn't retried. Learn more in the Error Handling chapter.

Retry Intervals#

A step is retried immediately after it fails. MedusaGo doesn't have the retryInterval option of Medusa, which turns a workflow into a long-running one that retries after a delay.

If a step needs to wait before it tries again, make the step wait. Use the Attempt property to calculate how long:

src/workflows/capture-payment.go
// captureStep waits before it tries again: a retry executes the step
// immediately, so the step waits for as long as it needs.
var captureStep = composer.CreateStep("capture-payment",
	func(ctx composer.StepExecutionContext, _ any) (composer.StepResponse[string], error) {
		if ctx.Attempt > 1 {
			// 2 seconds before the second attempt, 4 before the third one
			time.Sleep(time.Duration(ctx.Attempt) * time.Second)
		}

		// TODO capture the payment
		return composer.StepResponse[string]{}, errors.New("the payment provider is unavailable")
	}, nil).Config("", composer.StepOptions{MaxRetries: 2})

The step waits in the workflow's execution, so only use it for short delays.


Retry Waiting Async Steps#

An async step whose function returns an empty response stays in a waiting state until you set its status with composer.SetStepSuccess or composer.SetStepFailure. The MaxRetries option doesn't apply to a waiting step, because the step hasn't failed.

An async step can stay waiting indefinitely. For example:

  • The process running the step dies or restarts before the step finishes, so nothing sets the step's status.
  • The step starts a job in a third-party service, and you want to check the job's status until it finishes.

To execute a waiting step's function again, retry it manually, as explained in the next section. MedusaGo doesn't have the retryIntervalAwaiting and maxAwaitingRetries options of Medusa.


Manually Retry a Step#

In some cases, you may need to manually retry a step. For example:

  • If the step takes longer than expected to complete.
  • If the status of a third-party job it started isn't known yet.

To retry a waiting step manually, call composer.RetryStep with the container and the details of the step's execution.

For example, consider the following async step. Each time its function runs, it checks the status of an import job. If the job isn't completed, it returns an empty response, so the step stays waiting:

src/workflows/import-job.go
// waitForImportStep checks the status of an import job in a third-party
// service. While the job isn't completed, it returns an empty response, so
// the step stays waiting.
var waitForImportStep = composer.CreateStep("wait-for-import",
	func(ctx composer.StepExecutionContext, jobID string) (composer.StepResponse[any], error) {
		job := getImportJob(jobID, ctx.Attempt)
		if job != "completed" {
			return composer.StepResponse[any]{}, nil
		}

		return composer.NewStepResponse[any]("import completed"), nil
	}, nil).Config("", composer.StepOptions{Async: true})

// ImportWorkflow waits for an import job.
var ImportWorkflow = composer.CreateWorkflow("import-job",
	func(e *composer.Execution, jobID string) (any, error) {
		return waitForImportStep.Run(e, jobID)
	})

To execute the step's function again, call RetryStep:

src/workflows/retry-step.go
// RetryWaitForImport executes the handler of the waiting step again.
func RetryWaitForImport(ctx context.Context, container composer.Container, transactionID string) error {
	_, err := composer.RetryStep(ctx, container, composer.IdempotencyKey{
		Action:        "invoke",
		TransactionID: transactionID,
		StepID:        "wait-for-import",
		WorkflowID:    "import-job",
	})
	return err
}

RetryStep accepts the container and an IdempotencyKey with the following properties:

  • Action: invoke.
  • TransactionID: the ID of the workflow execution's transaction. Learn how to retrieve it in the Long-Running Workflows chapter.
  • StepID: the name of the step, as passed to composer.CreateStep.
  • WorkflowID: the ID of the workflow, as passed to composer.CreateWorkflow.

The step's function runs again, with its Attempt incremented. If it returns a response this time, the step completes and the workflow continues. If it returns an empty response, the step keeps waiting.

If the step isn't waiting for a response, RetryStep returns an error.

Note: The in-memory Workflow Engine Module keeps the waiting steps in the process that executes them, so you can only retry them from that process, and a restart loses them. The Redis Workflow Engine Module stores the workflow's checkpoint, so a step can be retried from any instance. Learn how to configure it in the MedusaGo Configurations chapter.
Found a problem with this page?Report Issue
Edited Nov 14·Edit this page