Workflow Hooks
In this chapter, you'll learn what workflow hooks are and how to use them.
What is a Workflow Hook?#
A workflow hook is a specific point in a workflow where you can inject custom functionality. This custom functionality is called a hook handler.
MedusaGo exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic. The hooks have the same names as in Medusa.
How to Consume a Hook?#
Every workflow created with composer.CreateWorkflow has a RegisterHook method. It accepts three parameters:
- The name of the hook.
- The hook handler: a step function.
- An optional compensation function. It's
nilif you don't need one.
So, to consume a hook:
- Import the workflow from its Go package in MedusaGo's core flows (
packages/core/core-flows/src). - Call
RegisterHookwith the name of the hook and the handler. - Call the function that registers the hook before the application starts, for example from
main.go.
For example, to consume the productsCreated hook of Medusa's CreateProductsWorkflow, create the file src/workflows/hooks/product-created.go with the following content:
// RegisterProductsCreatedHook consumes the productsCreated hook of
// createProductsWorkflow. Call it before the application starts, for
// example from main.go.
func RegisterProductsCreatedHook() {
workflows.CreateProductsWorkflow.RegisterHook("productsCreated",
func(ctx composer.StepExecutionContext, data any) (composer.StepResponse[any], error) {
// the hook's input is a record with the created products
input := data.(*dal.Record)
products, _ := input.Get("products")
logger := ctx.Container.Resolve(common.ContainerRegistrationKeys.LOGGER).(*slog.Logger)
logger.Info("Products created", "products", len(products.([]*dal.Record)))
return composer.NewStepResponse[any](nil), nil
},
nil)
}
Then, in main.go, call hooks.RegisterProductsCreatedHook() before you call cli.Run. A hook handler only runs in an application that registered it.
Now, when a product is created using the Create Product API route, your hook handler runs after the product is created.
CreateHook in its file under packages/core/core-flows/src in the MedusaGo repository. The call shows the name of the hook and its input.Hook Handler Parameters#
Since a hook handler is essentially a step function, it receives the step's context as a first parameter, and the hook's input as a second parameter. The context has a Container property to resolve resources from the container.
The hook's input has the type any. Each hook has a different input. For example, the productsCreated hook receives a *dal.Record with a products property that holds the created products, and an additional_data property.
The handler returns a composer.StepResponse and an error. If it returns an error, the workflow fails and the compensation functions are called.
Hook Handler Compensation#
Since the hook handler is a step function, you can set its compensation function as the third parameter of RegisterHook.
For example:
// RegisterProductsCreatedHookWithCompensation consumes the hook with a
// compensation function, the third argument of RegisterHook.
func RegisterProductsCreatedHookWithCompensation() {
workflows.CreateProductsWorkflow.RegisterHook("productsCreated",
func(ctx composer.StepExecutionContext, data any) (composer.StepResponse[any], error) {
// TODO perform an action
ids := []string{"id_1"}
return composer.NewStepResponse[any](nil, ids), nil
},
func(ctx composer.StepExecutionContext, data any) error {
ids, _ := data.([]string)
_ = ids
// TODO undo the performed action
return nil
})
}
The compensation function runs if an error occurs in the workflow. It undoes the actions performed by the hook handler.
The compensation function receives the second parameter passed to the StepResponse returned by the handler as input. Learn more in the Compensation Function chapter.
Additional Data Property#
Medusa's workflows include an additional_data property in the hook's input:
// RegisterProductsCreatedHookWithAdditionalData reads the additional data
// the hook's input holds.
func RegisterProductsCreatedHookWithAdditionalData() {
workflows.CreateProductsWorkflow.RegisterHook("productsCreated",
func(ctx composer.StepExecutionContext, data any) (composer.StepResponse[any], error) {
input := data.(*dal.Record)
// additional_data is nil if none was passed
additionalData, _ := input.Get("additional_data")
if additional, ok := additionalData.(*dal.Record); ok && additional != nil {
customField, _ := additional.Get("custom_field")
_ = customField
}
return composer.NewStepResponse[any](nil), nil
},
nil)
}
This property is a *dal.Record that contains additional data passed to the workflow through the request sent to the API route. It's nil if no additional data was passed.
The API routes that run workflows with hooks accept an additional_data object in the request body, and pass it to the workflow. MedusaGo doesn't validate the properties of additional_data: you validate them in a middleware or in the hook handler, as explained in the Extend Create Product Flow guide.
Pass Additional Data to Workflow#
You can also pass additional data when running the workflow. Set it in the workflow's input:
// POST runs createProductsWorkflow with additional data.
func POST(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
product := dal.NewRecord()
product.Set("title", "Shirt")
// ...
additionalData := dal.NewRecord()
additionalData.Set("custom_field", "test")
_, err := workflows.CreateProductsWorkflow.Run(req.Ctx(), req.Scope, workflows.CreateProductsInput{
Products: []*dal.Record{product},
AdditionalData: additionalData,
})
return err
}
Your hook handler then receives the passed data in the additional_data property.
Hooks with Return Values#
Some workflow hooks expect you to return data from the hook handler. This data is then used in subsequent steps of the workflow.
You return the data in the StepResponse the handler returns:
For example, the setPricingContext hook of CreateAndCompleteReturnOrderWorkflow merges the data its handler returns into the pricing context:
// RegisterPricingContextHook consumes a hook that uses the value its
// handler returns: setPricingContext of createAndCompleteReturnOrderWorkflow
// merges it into the pricing context.
func RegisterPricingContextHook() {
orderworkflows.CreateAndCompleteReturnOrderWorkflow.RegisterHook("setPricingContext",
func(ctx composer.StepExecutionContext, data any) (composer.StepResponse[any], error) {
pricingContext := dal.NewRecord()
pricingContext.Set("customer_group_id", "cusgroup_123")
return composer.NewStepResponse[any](pricingContext), nil
},
nil)
}
Refer to the source of a workflow in the MedusaGo repository to check whether a hook uses the data its handler returns.
Add Hooks to Your Workflow#
You can add hooks to your own workflows, so that other customizations, such as a plugin, can inject their logic. Call composer.CreateHook in the workflow's body, with the execution, the hook's name, and its input:
// CreateOrderWorkflow exposes the hook "orderCreated": the handler
// registered for it runs at that point, as a step of the workflow. Without a
// handler, the hook does nothing.
var CreateOrderWorkflow = composer.CreateWorkflow("create-order",
func(e *composer.Execution, input CreateOrderInput) (string, error) {
// TODO create the order
hookInput := dal.NewRecord()
hookInput.Set("order_id", input.OrderID)
if err := composer.CreateHook(e, "orderCreated", hookInput); err != nil {
return "", err
}
return input.OrderID, nil
})
If no handler is registered for the hook, it does nothing. If a handler is registered with CreateOrderWorkflow.RegisterHook, it runs at that point as a step of the workflow, and its compensation function runs if the workflow fails.
To use the data the handler returns, call composer.CreateHookWithResult. It returns the data, or nil if no handler is registered:
// PricingWorkflow uses the value a hook handler returns.
var PricingWorkflow = composer.CreateWorkflow("pricing",
func(e *composer.Execution, input CreateOrderInput) (*dal.Record, error) {
hookInput := dal.NewRecord()
hookInput.Set("order_id", input.OrderID)
result, err := composer.CreateHookWithResult(e, "setPricingContext", hookInput)
if err != nil {
return nil, err
}
// result is nil if no handler is registered
pricingContext, _ := result.(*dal.Record)
return pricingContext, nil
})
Hooks in Plugins#
A plugin consumes hooks in the Load function of its definition, which runs once when the plugin is loaded. Learn more in the Create a Plugin chapter.