# 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`:

```js
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`:

```js
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.

```json
{
  "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

```js
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:

```js
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:

```js
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`.

```js
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.SHOPIFY_STORE` and `env.SHOPIFY_API_VERSION`:

```js
export class Workflow {
  async start(data, headers, api) {
    const resp = await fetch(
      `https://${env.SHOPIFY_STORE}/admin/api/${env.SHOPIFY_API_VERSION}/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:

```js
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.FRAUD_API_KEY}` },
      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:
```json
{
  "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:

```js
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:

```json
"{\"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:

```graphql
query {
  runJsworkflow {
    data
  }
}
```

Use code like this:

```js
export default function main(input) {
  const parsed = JSON.parse(input.runJsworkflow.data);
  return { id: parsed.id };
}
```

Define outputs like this:

```graphql
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:

```js
return {
  success: true,
  score: 92,
  flagged: true,
  reviewTag: 'manual-review',
};
```

then your **Run code** step can parse and expose all of them:

```js
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:

```graphql
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.

```js
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.

```js
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:

```graphql
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:

```js
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.