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.
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
storeoption. If your workflow is a Long-Running Workflow, this option is enabled by default. - Set the
retentionTimeoption to the number of seconds that the workflow execution should be stored in the database.
For example:
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.
Retrieve Workflow Executions#
When you execute a workflow, the returned object has a transaction property containing the workflow execution's transaction details:
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:
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:
{
"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:
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:
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.
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
SkipExecutionErrorwith the messageTransaction 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
SkipExecutionErrorwith 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.
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:
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:
await helloWorkflow(container).run({
input: {},
context: {
transactionId: `webhook-${event.id}`,
},
})
To deduplicate executions reliably:
- Enable the
idempotentoption so that Medusa doesn't run the workflow again after an execution finishes. - Set the
retentionTimeoption 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
SkipExecutionErrorin 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.