Flow Builder
Build workflows with a visual node editor
medusa-flow-builder-plugin
Shopify-Flow-style visual workflow automation for Medusa v2.
- Draggable node editor in the admin dashboard — trigger → conditions → wait → actions.
- Copy to clipboard
.flowfile import and export that interchanges with Shopify Flow. Task IDs translate both directions. - Extensible task registry — register your own triggers, conditions, and actions from your app with Copy to clipboard
registerTask. - Durable WAIT — resumptions survive restarts via a persisted pending-resumption table + 1-minute cron sweeper.
- Built-in task catalog — 15 triggers, 2 control nodes (wait, condition), 13 actions covering orders, customers, products, webhooks, Slack, Customer.io, and log output.
Status: alpha. API may change. Not yet published to npm — for now, consume via Medusa's local-package workflow (yalc-based).
Install
Once published to npm
1yarn add medusa-flow-builder-plugin
Until then — local install via Medusa's plugin CLI
In the plugin repo:
12345yarn installyarn build # runs `medusa plugin:build` → writes .medusa/server + .medusa/adminyalc publish # publishes to your local yalc store# (the official `medusa plugin:publish` is broken in @medusajs/cli@2.13.3# — TypeError: cmd is not a function. Raw `yalc publish` does the same job.)
In the consumer Medusa app:
1234medusa plugin:add medusa-flow-builder-plugin# Adds `"medusa-flow-builder-plugin": "file:.yalc/medusa-flow-builder-plugin"` to package.json,# creates .yalc/medusa-flow-builder-plugin/, and writes yalc.lock.yarn install
Register in your app's Copy to clipboardmedusa-config.ts:
123456module.exports = defineConfig({plugins: [{ resolve: "medusa-flow-builder-plugin" },],// …})
Run the migration so the plugin's tables are created:
1yarn medusa db:migrate
Open the admin dashboard → sidebar Extensions → Flow Builder.
New devs: see Copy to clipboardHANDOVER.md for an end-to-end setup walkthrough that assumes zero Medusa experience.Built-in tasks
Kind Task ID Description Trigger Copy to clipboardmedusa::order::placed Order placed Trigger Copy to clipboardmedusa::order::paid Order paid Trigger Copy to clipboardmedusa::order::fulfilled Order fulfilled Trigger Copy to clipboardmedusa::order::canceled Order canceled Trigger Copy to clipboardmedusa::order::refunded Order refunded Trigger Copy to clipboardmedusa::order::updated Order updated Trigger Copy to clipboardmedusa::customer::created Customer created Trigger Copy to clipboardmedusa::customer::updated Customer updated Trigger Copy to clipboardmedusa::product::created Product created Trigger Copy to clipboardmedusa::product::updated Product updated Trigger Copy to clipboardmedusa::product::deleted Product deleted Trigger Copy to clipboardmedusa::cart::updated Cart updated Trigger Copy to clipboardmedusa::fulfillment::created Fulfillment created Trigger Copy to clipboardmedusa::inventory::level_changed Inventory level changed Trigger Copy to clipboardmedusa::flow::custom_webhook Public inbound webhook (Copy to clipboardPOST /hooks/flow-builder/:token) Control Copy to clipboardflow::wait Pause N seconds/minutes/hours/days before continuing Control Copy to clipboardflow::condition Route through Copy to clipboardif-then-true / Copy to clipboardif-then-false output ports Action Copy to clipboardmedusa::order::add_tags Append tags to order metadata Action Copy to clipboardmedusa::order::remove_tags Remove tags from order metadata Action Copy to clipboardmedusa::order::add_metafield Set a namespaced metafield on an order Action Copy to clipboardmedusa::order::set_metadata Merge keys into Copy to clipboardorder.metadata Action Copy to clipboardmedusa::order::cancel Cancel an order Action Copy to clipboardmedusa::customer::add_tags Append customer tags Action Copy to clipboardmedusa::customer::remove_tags Remove customer tags Action Copy to clipboardmedusa::customer::add_metafield Set a namespaced metafield on a customer Action Copy to clipboardmedusa::product::add_tags Append product tags Action Copy to clipboardmedusa::webhook::post POST JSON to a URL Action Copy to clipboardmedusa::slack::post POST a message to a Slack incoming webhook Action Copy to clipboardmedusa::customerio::send_email Send a Customer.io transactional message (requires Copy to clipboardcustomerio-node installed) Action Copy to clipboardmedusa::flow::log_output Log a templated line to the server logger
All action config fields support Copy to clipboard{{ payload.path.to.field }} template interpolation against the triggering event's payload.
Registering your own tasks
Create a file in your app that runs at boot time — a subscriber, a loader, or Copy to clipboardinstrumentation.ts:
1234567891011121314151617181920// src/subscribers/register-flow-tasks.tsimport { registerTask } from "medusa-flow-builder-plugin"registerTask({task_id: "acme::subscription::pause",task_version: "0.1",task_type: "ACTION",label: "Pause subscription",description: "Pauses the subscription referenced by the payload.",category: "Subscription",config_fields: [{ id: "subscription_id", label: "Subscription ID", type: "text", required: true },{ id: "reason", label: "Reason", type: "text" },],input_ports: [{ id: "input", label: "" }],output_ports: [{ id: "output", label: "" }],async execute(ctx, config) {const subscriptionService = ctx.container.resolve("subscription_service")await subscriptionService.pause(config.subscription_id, { reason: config.reason })return { status: "success" }
The task appears in the admin palette immediately. Re-registering the same Copy to clipboardtask_id overrides the previous definition, including built-ins.
For custom triggers bound to events Medusa doesn't fire natively, register the trigger AND write a subscriber that forwards the event:
12345678910111213141516171819import { registerTask, runFlowsForMedusaEvent } from "medusa-flow-builder-plugin"registerTask({task_id: "acme::billing::invoice_issued",task_version: "0.1",task_type: "TRIGGER",label: "Invoice issued",description: "Fires when our billing service issues an invoice.",category: "Trigger",config_fields: [],output_ports: [{ id: "output", label: "" }],trigger: { medusa_event: "acme.invoice_issued", payload_shape: { invoice_id: "string", amount: "number" } },})// In a subscriber:export default async function ({ event: { data }, container }) {await runFlowsForMedusaEvent("acme.invoice_issued", data, container)}export const config = { event: "acme.invoice_issued" }
The plugin already ships umbrella subscribers for the built-in triggers listed above.
Copy to clipboard.flow file import / export
The list page has an Import .flow button that accepts Shopify Flow exports. Shopify Copy to clipboardtask_ids are translated to their Medusa equivalents on import (see Copy to clipboardsrc/modules/flow_builder/interop/shopify-map.ts). Unmapped tasks are preserved as-is with an "unsupported" note — flows still open in the editor; unmapped steps are skipped at runtime.
Each row in the list page has an Export .flow button. Exports include a matching SHA-256 prefix of the JSON body. Task IDs reverse-translate to Shopify identifiers where a mapping exists, so the file drops back into Shopify Flow cleanly.
Durable WAIT
When a run hits a Copy to clipboardflow::wait step, it writes a row to Copy to clipboardflow_builder_pending_resumption (Copy to clipboardflow_run_id, Copy to clipboardstep_id, Copy to clipboardresume_at, Copy to clipboardnext_payload, Copy to clipboardvariables) and stops that branch. A cron job runs every minute, sweeps due resumptions, and re-enters the runner at the step downstream of the wait. Runs survive restarts.
Units supported: Copy to clipboardseconds, Copy to clipboardminutes, Copy to clipboardhours, Copy to clipboarddays. Resolution is 1 minute; sub-minute waits will fire on the next cron tick.
Local development
Medusa's plugin workflow is yalc-based. Each consuming app gets a copy of the built plugin under its own Copy to clipboard.yalc/ directory, with a Copy to clipboardfile: reference in Copy to clipboardpackage.json — no npm registry needed during development.
12345678910git clone https://github.com/Rx-Ventures/medusa-flow-builder-plugincd medusa-flow-builder-pluginyarn installyarn build # medusa plugin:buildyalc publish # publishes to ~/.yalc# In your Medusa app:medusa plugin:add medusa-flow-builder-pluginyarn installyarn dev
For the rapid edit-loop while developing the plugin:
1yarn dev # medusa plugin:develop — watch + auto-republish
If you don't want the watcher, do it manually after each change:
1yarn build && yalc push # rebuild + push to every yalc consumer
Heads up: Copy to clipboardmedusa plugin:publishfrom Copy to clipboard@medusajs/cli@2.13.3errors with Copy to clipboardTypeError: cmd is not a function. As a workaround, run Copy to clipboardyarn buildfollowed by raw Copy to clipboardyalc publish— same end result. The bug appears fixed in Copy to clipboard2.14.x; we'll switch back to the official command once the consumer app upgrades.
Heads up #2: Copy to clipboardyalc pushonly copies plugin files into the consumer's Copy to clipboard.yalc/directory — it does not re-resolve transitive Copy to clipboarddependencies. If you change the plugin's Copy to clipboarddependenciesfield, the consumer needs to re-fetch them:
12# in the consuming app:yalc update && yarn install --check-files
Publishing to npm (when ready)
12yarn buildnpm publish --access public # or `npm publish` for a private/scoped package
The Copy to clipboardprepublishOnly script re-runs the build for you, so consumers always get the compiled output. Only the contents of the Copy to clipboardfiles whitelist (Copy to clipboard.medusa/server, Copy to clipboard.medusa/admin, Copy to clipboardpackage.json, Copy to clipboardREADME.md, Copy to clipboardLICENSE) end up in the published tarball — source Copy to clipboardsrc/ is not shipped.
Testing
1yarn test:unit
The suite covers: condition evaluation, template interpolation, WAIT duration parsing, Copy to clipboard.flow import/export round-trip, the Shopify↔Medusa task translation, and the Copy to clipboardregisterTask extensibility hook.
License
MIT © Rx-Ventures

