Shopify Flow Run a JsWorkflows workflow as an action step inside a Shopify Flow automation, and return data back into the Flow. The Shopify Flow integration works in both directions: - Flow action: Shopify Flow can call a JsWorkflows workflow as the Run JsWorkflow action - Flow trigger source: JsWorkflows can fire JsWorkflow Data Received, JsWorkflow Run Completed, and JsWorkflow Run Failed events into Shopify Flow Using the Run JsWorkflow action Use the Run JsWorkflow action when a Shopify Flow automation needs custom logic, external processing, or other work that is better handled in JsWorkflows. Flow calls the workflow synchronously, and the workflow can return data back into Flow for later conditions, actions, or Run code steps. Common use cases The Run JsWorkflow action is most useful when Shopify Flow should hand work off to JsWorkflows at that point in the automation, whether or not Flow needs a value returned immediately. Common examples include: - starting a custom operational workflow from Flow, then letting JsWorkflows handle the work from that point - handing off heavier or multi-step operational work to JsWorkflows once a Flow condition is met - running business logic that is easier to maintain in JavaScript than in Flow conditions - calling external systems from a Flow automation, especially when the request or response needs custom handling - returning a decision value or structured result back into Flow for later conditions or actions - centralizing reusable logic so multiple Flow automations can call the same JsWorkflow instead of duplicating rules In practice, this action is strongest when Flow decides when the work should start, and JsWorkflows handles how that work should run. How it works There are two sides to the Flow action integration: - JsWorkflows side: create a workflow with the Flow action trigger type. This exposes it as a selectable action in Shopify Flow. - Shopify Flow side: add the Run JsWorkflow action to any Flow automation and select which workflow to run. Shopify calls it synchronously and makes the workflow's return value available as a variable in subsequent steps. Step 1: Create a Flow action workflow 1. In JsWorkflows, click Create workflow. 2. Under Build a custom workflow, select Flow action. 3. Give the workflow a clear name. This is what you will see in the Flow editor dropdown when selecting which workflow to run. 4. Save and activate the workflow. Caution: Only workflows with the Flow action trigger type appear in the Shopify Flow action selector. Shopify Webhook, HTTP Trigger, and other types are not listed. Step 2: Add the action in Shopify Flow 1. Open Shopify Admin → Apps → Flow and create or edit an automation. 2. Add an action step and search for Run JsWorkflow. 3. Click Select workflow. A configuration panel opens where you choose which JsWorkflows workflow to run. 4. Optionally fill in the Additional Payload field to pass data from preceding Flow steps into your workflow. Click Add variable inside the field to select data from earlier steps in the automation. 5. Save the automation. The data object When Shopify Flow triggers the workflow, data is populated from the Additional Payload field you configured in the action step. How you access the data depends on what you put in that field. When the payload is a valid JSON object (most common) If the Additional Payload field contains valid JSON, the fields are available directly on data: export class Workflow { async start(data, headers, api) { console.log('Order ID:', data.orderId); console.log('Customer email:', data.customerEmail); } } If the Additional Payload field is left empty, data is an empty object ({}). When the payload is a plain string If you intentionally pass a plain string instead of a JSON object, it is available at data.payload: export class Workflow { async start(data, headers, api) { console.log('String value:', data.payload); // e.g. "hello" } } This also applies when the Additional Payload field contains invalid JSON. For example, entering {id:"{{order.id}}"} instead of {"id":"{{order.id}}"} produces invalid JSON (keys must be quoted), so JsWorkflows cannot parse it as an object. The raw string falls through to data.payload instead of being available at data.id. Caution: A common mistake is forgetting to quote JSON keys. {id:"123"} is not valid JSON — it must be {"id":"123"}. If your workflow receives undefined for an expected field, check that your Additional Payload is well-formed JSON. Use a JSON validator if you are unsure. Additional Payload field The Additional Payload field accepts a JSON object. Type the JSON structure you want, and use the Add variable button to insert dynamic values from preceding steps in the automation, such as the order ID, customer email, or any other field available in the Flow trigger event. { "orderId": "{{order.id}}", "customerEmail": "{{order.email}}", "totalPrice": "{{order.totalDiscountsSet.shopMoney.amount}}" } This is the primary way to pass data from the Flow trigger event into your workflow. Note: The Additional Payload is optional. If your workflow only needs to run some fixed logic (send a notification, update a record, call an API) without needing input from the Flow trigger event, you can leave it empty. Returning data to Shopify Flow The return value of the start step is passed back to Shopify Flow as an output named runJsworkflow, available in the variable picker under the Run JsWorkflow action. The output contains a data field of type String. This field holds a JSON-encoded string of whatever your start step returned, and is available in the variable picker for use in subsequent Flow steps. Returning a plain object export class Workflow { async start(data, headers, api) { const result = await fetch('https://your-api.example.com/orders/' + data.orderId); const order = await result.json(); return { status: order.status, trackingUrl: order.trackingUrl, eta: order.estimatedDelivery, }; } } runJsworkflow.data in Flow contains: {"status":"shipped","trackingUrl":"https://...","eta":"2025-06-01"} Returning a Response You can also return a Response.json() directly, which gives you the same result: async start(data, headers, api) { return Response.json({ status: 'shipped', trackingUrl: 'https://tracking.example.com/123', eta: '2025-06-01', }); } Returning a single value Wrap single values in an object so they have a named key: async start(data, headers, api) { const score = await getRiskScore(data.orderId); return { riskScore: score }; } runJsworkflow.data will contain {"riskScore":92}, available as a string in subsequent Flow steps. Returning nothing If the start step returns nothing, runJsworkflow.data is null in Flow. This is expected when the workflow performs a side-effect (send an email, tag a customer) and does not need to pass anything back. In practice, this also applies when start returns undefined or null. async start(data, headers, api) { await tagCustomer(data.customerId, 'vip'); // no return — runJsworkflow.data is null } Caution: Only the start step's return value is sent back to Flow. Any subsequent steps scheduled with api.scheduleNextStep() run asynchronously after Shopify has already received the response. Their return values are not visible to Flow. If you need data from an external API in Flow, make that API call inside the start step and return the result directly. Calling the Shopify API The Shopify access token is injected automatically. Use env.SHOPIFYSTORE and env.SHOPIFYAPIVERSION: export class Workflow { async start(data, headers, api) { const resp = await fetch( https://${env.SHOPIFYSTORE}/admin/api/${env.SHOPIFYAPIVERSION}/graphql.json, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: mutation AddTag($id: ID!, $tags: [String!]!) { tagsAdd(id: $id, tags: $tags) { node { id } userErrors { field message } } }, variables: { id: gid://shopify/Customer/${data.customerId}, tags: ['vip'], }, }), } ); const { data: gqlData } = await resp.json(); const { userErrors } = gqlData.tagsAdd; if (userErrors.length) { console.log('GraphQL errors:', userErrors); return { success: false, errors: userErrors.map(e => e.message) }; } return { success: true, customerId: data.customerId }; } } Example: enrich order with fraud score and return it to Flow This workflow calls an external fraud-scoring API in start, returns the score to Flow so a subsequent condition can branch on it, and schedules heavier logging asynchronously: export class Workflow { async start(data, headers, api) { // data.orderId and data.totalPrice were set via the Additional Payload field in Flow const scoreResp = await fetch('https://fraud-api.example.com/score', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': Bearer ${env.FRAUDAPIKEY} }, body: JSON.stringify({ orderId: data.orderId, total: data.totalPrice }), }); const { score, reason } = await scoreResp.json(); // Schedule detailed logging in the background (does not block the Flow response) await api.scheduleNextStep({ delay: 10, action: 'logResult', payload: { orderId: data.orderId, score, reason }, }); // This is what Flow's variable picker sees as runJsworkflow.data return { score, reason, flagged: score > 80 }; } async logResult(data, headers, api) { // Runs after Flow has already received the score console.log(Order ${data.orderId}: fraud score ${data.score} (${data.reason})); if (data.flagged) { await fetch('https://hooks.slack.com/services/...', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: Order ${data.orderId} flagged with score: ${data.score} }), }); } } } In Shopify Flow, set the Additional Payload using Add variable to insert the order ID and total price from the trigger event: { "orderId": "{{order.id}}", "totalPrice": "{{order.totalDiscountsSet.shopMoney.amount}}" } After the action runs, runJsworkflow.data contains {"score":92,"reason":"high-velocity","flagged":true}, available as a string in subsequent Flow steps. Using the action output in Shopify Flow runJsworkflow.data is returned to Flow as a string, not as a structured object. That means you cannot reliably use score, id, trackingUrl, or other nested fields directly in later Flow steps until you parse the JSON string first. The clearest pattern is: 1. Run the Run JsWorkflow action 2. Add a Run code step immediately after it 3. Parse runJsworkflow.data 4. Return the specific fields you want to use later in Flow Example: extract an id field If your workflow returns: async start(data, headers, api) { return { id: 'gid://shopify/Product/1234567890' }; } then Shopify Flow receives that value in: - runJsworkflow.data but the value is still a JSON string: "{\"id\":\"gid://shopify/Product/1234567890\"}" To make id usable in later Flow steps, add a Run code step after the action. Use this input query: query { runJsworkflow { data } } Use code like this: export default function main(input) { const parsed = JSON.parse(input.runJsworkflow.data); return { id: parsed.id }; } Define outputs like this: type Output { "ID extracted from the JsWorkflow result" id: ID! } After that, later Flow steps can use: - {{runCode.id}} This is usually the best way to expose returned values cleanly into the rest of the automation. Example: extract multiple fields If your workflow returns: return { success: true, score: 92, flagged: true, reviewTag: 'manual-review', }; then your Run code step can parse and expose all of them: export default function main(input) { const parsed = JSON.parse(input.runJsworkflow.data); return { success: parsed.success, score: parsed.score, flagged: parsed.flagged, reviewTag: parsed.reviewTag, }; } with outputs such as: type Output { success: Boolean! score: Int flagged: Boolean! reviewTag: String } Later Flow steps can then branch or act on: - {{runCode.success}} - {{runCode.score}} - {{runCode.flagged}} - {{runCode.reviewTag}} Note: If your workflow may return null, no value, or an error-shaped result, make your Run code step defensive. For example, check that input.runJsworkflow.data exists before calling JSON.parse(...). Error handling The Run JsWorkflow action still moves to the next step in Flow even when your workflow returns an error status. Do not rely on HTTP status codes to control Flow branching. Instead, return a structured result such as { success: false, error: '...' } and add a Condition step in Flow that branches on that result. If your start step returns nothing (or undefined or null), runJsworkflow.data is null. Flow still advances to the next step. There is no automatic failure signal. A null result looks the same to Flow as a deliberate no-op. This means error signalling must happen explicitly inside the data you return. Use a field like success or error in your return value, then add a Condition step in Flow that branches on that field. async start(data, headers, api) { if (!data.orderId) { return { success: false, error: 'orderId is required' }; } try { const resp = await fetch('https://fraud-api.example.com/score', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId: data.orderId }), }); if (!resp.ok) { return { success: false, error: Fraud API returned ${resp.status} }; } const result = await resp.json(); return { success: true, score: result.score }; } catch (err) { return { success: false, error: err.message }; } } In Flow, add a Condition after the action that checks whether runJsworkflow.data contains "success":true before continuing with the happy path. The error field is available in the same string for a failure branch. Note: An unhandled exception in start marks the JsWorkflows run as failed internally and fires the JsWorkflow Run Failed trigger. However, from Flow's perspective the action step still completes. Flow does not see it as a failure and will continue to the next step. JsWorkflows as a Shopify Flow trigger source Instead of Shopify Flow calling a JsWorkflows workflow as an action, JsWorkflows can also push events into Shopify Flow and start automations there. This is useful when a workflow needs to notify Flow about an important result, when a run completes or fails, or when a background step produces data that should drive a downstream Flow automation. JsWorkflow Data Received This trigger fires when a workflow step calls api.sendToFlow(data) to push data into Shopify Flow. Use it when your workflow produces useful data after it has already started running, especially in later or background steps, and you want Shopify Flow to continue with conditions, notifications, or other follow-up actions. The main use case is pushing a result into Flow from a background step, after the start step has already returned its response to the caller. export class Workflow { async start(data, headers, api) { await api.scheduleNextStep({ delay: 30, action: 'enrich', payload: { orderId: data.orderId } }); return { queued: true }; } async enrich(data, headers, api) { const resp = await fetch('https://fraud-api.example.com/score', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId: data.orderId }), }); const result = await resp.json(); await api.sendToFlow({ score: result.score, flagged: result.score > 80 }); } } Common use cases JsWorkflow Data Received is most useful when the important result only becomes available after JsWorkflows has done work that Shopify Flow cannot do well on its own. Common examples include: - human-in-the-loop workflows where a JsWorkflow waits for a review, approval, or external signal, then sends the outcome into Flow - running a delayed or background step and only later sending the final outcome into Flow - enriching orders, customers, or products with data from supplier, ERP, WMS, PIM, fraud, or other third-party systems - processing files, spreadsheets, or email attachments in JsWorkflows, then sending summary data or extracted values into Flow - pushing reconciliation or exception results into Flow after a multi-step sync or validation workflow finishes In practice, this trigger is strongest when JsWorkflows does the hard operational work first, and Shopify Flow handles the downstream conditions, notifications, tagging, or other merchant-facing actions. Available fields | Field | Type | Description | | workflowName | String | Name of the workflow that called api.sendToFlow() | | workflowId | String | ID of the workflow that called api.sendToFlow() | | runId | String | Unique ID for this specific run | | stepAction | String | Name of the step that called it (e.g. enrich, start) | | data | String | String payload sent by api.sendToFlow(). If you pass an object, JsWorkflows JSON-encodes it automatically. | Use workflowName or workflowId in a Flow Condition step to filter the automation to a specific workflow. Setting up 1. Open Shopify Admin → Apps → Flow and create a new automation. 2. Click Select a trigger and search for JsWorkflow Data Received. 3. Add a Condition step on workflowName or workflowId to target a specific workflow. 4. Add a Run code step to parse the data field and extract the values you need. 5. Save and activate the automation. Using the data in a Run code step Add a Run code step after the trigger. The fields are accessed flat in the input query: query { workflowName workflowId runId stepAction data } If your workflow passed an object to api.sendToFlow(...), the data field will contain JSON and you can parse it in the code block: export default function main(input) { const parsed = JSON.parse(input.data); return { score: parsed.score, flagged: parsed.flagged, }; } If your workflow passed a plain string instead, Flow receives that plain string directly in data. In that case, do not use JSON.parse(...) unless the string itself contains valid JSON. JsWorkflow Run Completed Fires when any workflow run finishes successfully, regardless of what originally started it (Shopify webhook, HTTP trigger, scheduled trigger, email trigger, or Flow action). Available fields | Field | Type | Description | | workflowId | String | ID of the workflow that ran | | runId | String | Unique ID for this specific run | | triggerType | String | What started the run (shopify-webhook, http-trigger, scheduled-trigger, email-trigger, flow-trigger) | | startedAt | String | Unix timestamp in milliseconds when the run started | | durationMs | String | Total run duration in milliseconds | Setting up 1. Open Shopify Admin → Apps → Flow and create a new automation. 2. Click Select a trigger and search for JsWorkflow Run Completed. 3. Add conditions and actions as needed. All fields above are available in the variable picker. 4. Save and activate the automation. JsWorkflow Run Failed Fires when any workflow run fails with an error, regardless of what originally started it. Available fields | Field | Type | Description | | workflowId | String | ID of the workflow that ran | | runId | String | Unique ID for this specific run | | triggerType | String | What started the run (shopify-webhook, http-trigger, scheduled-trigger, email-trigger, flow-trigger) | | startedAt | String | Unix timestamp in milliseconds when the run started | | durationMs | String | Total run duration in milliseconds | | error | String | The error message that caused the run to fail | Setting up 1. Open Shopify Admin → Apps → Flow and create a new automation. 2. Click Select a trigger and search for JsWorkflow Run Failed. 3. Add conditions and actions as needed. All fields above are available in the variable picker. 4. Save and activate the automation. Example: notify on failure A common use case is sending an alert whenever a workflow fails. Add a Send internal email action using the error and workflowId fields to describe what went wrong. You can also add a Condition step that checks workflowId so the automation only fires for a specific workflow.