4.8.11. Store Workflow Executions

In this chapter, you'll learn how to store workflow executions in the database, access them later, and use a workflow's transaction ID to avoid duplicate executions.

Workflow Execution Retention#

Medusa doesn't store your workflow's execution details by default. However, you can configure a workflow to keep its execution details stored in the database.

This is useful for auditing and debugging purposes. When you store a workflow's execution, you can view details around its steps, their states and their output. You can also check whether the workflow or any of its steps failed.

Tip: You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows.

How to Store Workflow's Executions?#

createWorkflow from the Workflows SDK can accept an object as a first parameter to set the workflow's configuration. To enable storing a workflow's executions:

  • Enable the store option. If your workflow is a Long-Running Workflow, this option is enabled by default.
  • Set the retentionTime option to the number of seconds that the workflow execution should be stored in the database.

For example:

Code
import { createStep, createWorkflow } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  {
    name: "step-1",
  },
  async () => {
    console.log("Hello from step 1")
  }
)

export const helloWorkflow = createWorkflow(
  {
    name: "hello-workflow",
    retentionTime: 99999,
    store: true,
  },
  () => {
    step1()
  }
)

Whenever you execute the helloWorkflow now, its execution details will be stored in the database.

Tip: Storing a workflow's executions also allows you to cancel an execution by its transaction ID after it's done.

Retrieve Workflow Executions#

Tip: You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows.

When you execute a workflow, the returned object has a transaction property containing the workflow execution's transaction details:

Code
const { transaction } = await helloWorkflow(container).run()

To retrieve a workflow's execution details from the database, resolve the Workflow Engine Module from the container and use its listWorkflowExecutions method.

For example, you can create a GET API Route at src/workflows/[id]/route.ts that retrieves a workflow execution for the specified transaction ID:

src/workflows/[id]/route.ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { transaction_id } = req.params
  
  const workflowEngineService = req.scope.resolve(
    Modules.WORKFLOW_ENGINE
  )

  const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({
    transaction_id: transaction_id,
  })

  res.json({
    workflowExecution,
  })
}

In the above example, you resolve the Workflow Engine Module from the container and use its listWorkflowExecutions method, passing the transaction_id as a filter to retrieve its workflow execution details.

A workflow execution object will be similar to the following:

Code
{
  "workflow_id": "hello-workflow",
  "transaction_id": "01JJC2T6AVJCQ3N4BRD1EB88SP",
  "id": "wf_exec_01JJC2T6B3P76JD35F12QTTA78",
  "execution": {
    "state": "done",
    "steps": {},
    "modelId": "hello-workflow",
    "options": {},
    "metadata": {},
    "startedAt": 1737719880027,
    "definition": {},
    "timedOutAt": null,
    "hasAsyncSteps": false,
    "transactionId": "01JJC2T6AVJCQ3N4BRD1EB88SP",
    "hasFailedSteps": false,
    "hasSkippedSteps": false,
    "hasWaitingSteps": false,
    "hasRevertedSteps": false,
    "hasSkippedOnFailureSteps": false
  },
  "context": {
    "data": {},
    "errors": []
  },
  "state": "done",
  "created_at": "2025-01-24T09:58:00.036Z",
  "updated_at": "2025-01-24T09:58:00.046Z",
  "deleted_at": null
}

Example: Check if Stored Workflow Execution Failed#

To check if a stored workflow execution failed, you can check its state property:

Code
if (workflowExecution.state === "failed") {
  return res.status(500).json({
    error: "Workflow failed",
  })
}

Other state values include done, invoking, and compensating.


Run a Workflow with an Existing Transaction ID#

When you execute a workflow, you can pass a transactionId in the context parameter of the run method to set the ID of the execution's transaction. If you don't pass one, Medusa generates a unique ID for every execution.

For example:

Code
const { result } = await helloWorkflow(container).run({
  input: {},
  context: {
    transactionId: "hello-123",
  },
})

If the workflow stores its executions, Medusa looks up an existing execution with the same transaction ID before running the workflow. What happens next depends on the state of that execution, as explained in the following sections.

Note: This behavior only applies to workflows that store their executions. Medusa stores a workflow's executions if you enable its store, retentionTime, or idempotent options, or if it's a Long-Running Workflow, has a timeout, or has steps with a timeout or retry interval. Otherwise, Medusa runs the workflow from the start every time, even if you pass the same transaction ID.

Execution Is Still Running#

If an execution with the same transaction ID is still in progress, Medusa loads the execution from its last saved checkpoint and resumes it instead of starting a new execution.

Medusa doesn't run the steps that already finished again, and it leaves steps that are waiting for a status change as they are. For example, if a Long-Running Workflow is waiting for an async step, the step keeps waiting until you change its status with the Workflow Engine Module's setStepSuccess or setStepFailure methods.

Concurrent Executions with the Same Transaction ID#

If you start two executions of a workflow with the same transaction ID at the same time, Medusa doesn't run the workflow twice. Instead, the second run call either:

  • Throws a SkipExecutionError with the message Transaction already started for transactionId: {id}. This happens when both calls start before either of them saves the execution.
  • Loads the execution that the first call saved and continues it, as explained in the previous section. Medusa doesn't run the steps that the first call already finished or is still running. In this case, the second call may also throw a SkipExecutionError with a different message if the first call moves ahead of it.

This prevents duplicate executions, for example, when a client retries a request or a third-party service sends the same webhook twice.

Warning: The In-Memory Workflow Engine only detects concurrent executions within the same process. If you run multiple Medusa instances, use the Redis Workflow Engine so that all instances share the same executions.

Execution Has Finished#

When an execution finishes, Medusa deletes it unless you set the workflow's retentionTime option. So, if you run the workflow again with the same transaction ID, Medusa runs it from the start as a new execution.

Setting the retentionTime option alone keeps the finished execution stored, but doesn't prevent running the workflow again. Medusa still runs the workflow from the start and stores the new execution with the same transaction ID, but a different run ID.

To return the finished execution instead of running the workflow again, enable the idempotent option and set the retentionTime option:

Code
export const helloWorkflow = createWorkflow(
  {
    name: "hello-workflow",
    idempotent: true,
    retentionTime: 86400,
  },
  () => {
    step1()
  }
)

Now, when you run helloWorkflow with the transaction ID of an execution that finished, Medusa returns the existing execution without running its steps again. This applies to executions that completed successfully, failed, or reverted. If the existing execution failed or reverted, the run method throws the execution's original error again, unless you set its throwOnError option to false.

Once the retention time passes and Medusa deletes the execution, running the workflow with the same transaction ID starts a new execution.

Use the Transaction ID as an Idempotency Key#

Based on the behavior above, you can use the transaction ID as an idempotency key to make sure a workflow runs only once for the same operation. For example, use the ID of a webhook event as the transaction ID, so that receiving the same event multiple times doesn't run the workflow multiple times:

Code
await helloWorkflow(container).run({
  input: {},
  context: {
    transactionId: `webhook-${event.id}`,
  },
})

To deduplicate executions reliably:

  • Enable the idempotent option so that Medusa doesn't run the workflow again after an execution finishes.
  • Set the retentionTime option to at least the period during which duplicates may arrive. After the retention time passes, Medusa no longer deduplicates executions with that transaction ID.
  • Use the Redis Workflow Engine in production so that all Medusa instances detect concurrent executions.
  • Handle the SkipExecutionError in your code, since a concurrent duplicate may throw it instead of returning a result. You can treat this error as a sign that another execution is already handling the operation.
Was this chapter helpful?
Ask Bloom
For assistance in your development, use Claude Code Plugins or Docs MCP server in Cursor, VSCode, etc...FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break
⇧↵