> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firmhouse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Script Execution workflow

> Create a Script Execution workflow that runs custom JavaScript from outgoing webhook events or a weekly schedule and returns Firmhouse actions for automation.

Script Execution lets you run custom JavaScript from an outgoing webhook event or a weekly schedule. Your script receives subscription or project context as `input` and must return one or more supported Firmhouse actions.

<Info>
  Script Execution is currently a private beta feature. During the beta, Firmhouse staff needs to enable it for your project.
</Info>

> **Note:** Contact Firmhouse Support to enable this feature for your project. This feature is only available for Advanced or higher tier merchants.

## What it is

Script Execution is useful when you need custom logic that is not covered by the standard workflow presets. An outgoing webhook can start the workflow when its selected event happens or at a configured weekly time. Instead of directly changing Firmhouse data from the script, your script decides which supported action Firmhouse should execute next.

The script:

* must contain valid JavaScript
* receives subscription or project context and trigger details as `input`
* must `return` an array of action objects
* cannot perform direct side effects in Firmhouse by itself

## Add a Script Execution workflow

1. Ask the Firmhouse support team to enable the **Script Execution** beta for your project.
2. Open your project in the Firmhouse portal.
3. In the sidebar, go to **Workflows**.
4. Click **Create new workflow**.
5. Choose **Script Execution**.
6. Enter a name for the workflow and optionally add a description.
7. Paste your JavaScript into the script editor.
8. Optionally, add **Input Parameters** (see below).
9. Click **Save workflow**.
10. In the sidebar, go to **Apps**.
11. Find the **Webhooks** app and click **Configure**.
12. Click **New outgoing webhook**.
13. Set **Content type** to **Script Execution**.
14. Select the Script Execution workflow you created.
15. Select the **Event** that should run the script.
16. Optionally, add a JSON **Template**. The rendered template is passed to the script as `input.trigger.payload`.
17. Make sure the webhook is enabled and click **Save**.

## Delay a signup workflow

This option is in a staff-managed preview. Firmhouse staff can enable **Script trigger
delays** for your project in **New Features**. It is off by default. Turning the feature
off skips delayed triggers, including executions still waiting; it does not run them immediately.

For **Subscription signed up**, **Subscription activated**, and **Order scheduled** events,
use **Run after** on the outgoing webhook to wait before running the script. Choose
**Immediately** (the default), or a delay of **1, 5, 10, 20, 30, or 60 minutes**.

For example, select **5 minutes** to let normal signup processing finish before adjusting
shipment or billing dates. Firmhouse builds the payload from current records after the
delay, rather than using dates captured when the event first occurred. Queue processing
can make execution start later than the selected delay.

Disabling or deleting the trigger before its delayed payload is generated cancels that
pending execution. Changing the delay affects new events; it does not reschedule events
already waiting. Delivery retries reuse the generated payload and do not wait again.
The event log appears once delivery begins. Manual test events run immediately.

Other event types, including **Order pre-confirmation**, must run immediately. A delay
does not guarantee that other processing succeeded: scripts should still check the
current order and subscription state before returning actions.

## Run a Script Execution workflow on a schedule

Use a scheduled Script Execution when the workflow should run once a week without waiting for a subscription event. Scheduled workflows run with project context rather than subscription context.

1. Create and save a Script Execution workflow.
2. In the sidebar, go to **Apps**.
3. Find the **Webhooks** app and click **Configure**.
4. Click **New outgoing webhook**.
5. Set **Content type** to **Script Execution**.
6. Select the Script Execution workflow you created.
7. Set **Event** to **Scheduled**.
8. Under **Repeats every**, select the weekday and time.
9. Make sure the webhook is enabled and click **Save**.

The time is evaluated in the `Europe/Amsterdam` timezone. The form displays `CET` or `CEST` as appropriate, and daylight saving time changes are applied automatically.

<Note>
  Scheduled runs are not connected to a subscription. They support the project-level actions `no_action` and `add_product_to_plan_group`. Actions that require subscription context cannot be returned from a scheduled run.
</Note>

Use this side-effect-free script to test a schedule:

```javascript theme={null}
return [
  {
    function: "no_action",
    arguments: {
      reason: `Scheduled run for ${input.project.name} at ${input.trigger.scheduled_for}`
    }
  }
]
```

## Input Parameters

Input Parameters let you define key/value pairs that become available in your script. This lets you write a single reusable script and configure it differently per workflow — without editing JavaScript.

Your script receives the parameters in two forms:

* `input.params` — the raw array of parameter objects exactly as configured, e.g. `[{"key": "order_1", "type": "product", "value": "gid://..."}, ...]`. Use this when you need access to the full metadata (type, individual entries).
* `input.params_normalized` — a convenience hash that groups values by key, e.g. `{"order_1": "gid://...", "order_2": ["gid://...", "gid://..."]}`. Duplicate keys are automatically collected into arrays. Use this for simple key-based lookups.

To add parameters, expand the **Input Parameters** section on the workflow form, click **+ Add parameter**, enter a key and value, and save.

### Parameter types

Each parameter has a **Type** selector:

* **Text** (default) — Enter any plain text value. The value is always passed to your script as a string. If you need array-like behaviour, use a comma-separated or pipe-separated value and split it in your script (e.g. `"a,b,c".split(",")`).
* **Product** — Select a product from your catalog using a search autocomplete field. This makes it easy to deterministically reference a specific product in your input parameters without needing to look up its ID. The selected product's Global ID is stored and passed to your script as a string (e.g. `"gid://firmhouse/Product/123"`).

You can use the same key multiple times. For example, two rows with key `order_2` and type Product will appear as two separate entries in `input.params`. In `input.params_normalized`, they are grouped into an array: `input.params_normalized.order_2` → `["gid://firmhouse/Product/101", "gid://firmhouse/Product/102"]`.

## The script protocol

Your script must return an array. Each item in the array must be an object with:

* `function`
* `arguments`

For example:

```javascript theme={null}
return [
  {
    function: "no_action",
    arguments: {
      reason: `No follow-up action is needed for subscription signed up at ${input.signed_up_at}`
    }
  }
]
```

### Rules

* The script must use valid JavaScript syntax.
* The script must `return [...]`, not `{ actions: [...] }`.
* The returned value must be an array.
* Each action must be an object.
* `function` must be one of the supported function names listed below.
* `arguments` must be an object.
* Required arguments must be present.
* Extra arguments are rejected.
* Argument types must match the expected types.

## Copy-paste examples

### No-op / smoke test

Use this script to verify that the workflow executes successfully without changing the subscription.

```javascript theme={null}
return [
  {
    function: "no_action",
    arguments: {
      reason: `signed up ${input.signed_up_at}`
    }
  }
]
```

### Product swap example

Use this script as a starting point for a workflow that supports multiple source-to-target product mappings and returns one swap action for each matching current product.

```javascript theme={null}
const productMappings = [
  {
    source_product_id: "gid://firmhouse/Product/101",
    target_product_id: "gid://firmhouse/Product/201"
  },
  {
    source_product_id: "gid://firmhouse/Product/102",
    target_product_id: "gid://firmhouse/Product/202"
  }
]

const actions = productMappings.flatMap((mapping) => {
  const matchingProduct = input.current_products.find((product) => {
    return product.id === mapping.source_product_id
  })

  if (!matchingProduct) return []

  return [
    {
      function: "swap_product_by_id",
      arguments: {
        current_product_id: matchingProduct.id,
        new_product_id: mapping.target_product_id,
        reason: `Swap ${matchingProduct.title} based on configured product mapping`
      }
    }
  ]
})

if (actions.length === 0) {
  return [
    {
      function: "no_action",
      arguments: { reason: "No configured source products were found on this subscription" }
    }
  ]
}

return actions
```

### Promotion by confirmed orders count

Use this script when you want to apply different promotions after specific order milestones. In this example, one promotion is applied after 3 confirmed orders and another after 6 confirmed orders.

```javascript theme={null}
const promotionByConfirmedOrders = {
  3: "gid://firmhouse/Promotion/301",
  6: "gid://firmhouse/Promotion/302"
}

const promotionId = promotionByConfirmedOrders[input.confirmed_orders_count]

if (!promotionId) {
  return [
    {
      function: "no_action",
      arguments: {
        reason: `No promotion configured for ${input.confirmed_orders_count} confirmed orders`
      }
    }
  ]
}

return [
  {
    function: "apply_promotion",
    arguments: {
      promotion_id: promotionId,
      reason: `Apply the configured promotion after ${input.confirmed_orders_count} confirmed orders`
    }
  }
]
```

### Add a free one-off product on a configured order

Use this script when you want to add a free product to a specific upcoming order. The product is added as a one-off add-on, so it is included in that order only.

For example, to ship a loyalty reward with order 3, configure the workflow to add it on order 3. Script Execution runs after an order is confirmed, so the action is returned after order 2 is confirmed and prepares the subscription for order 3.

Configure the Input Parameters like this:

| Key              | Type    | Value                                                           |
| ---------------- | ------- | --------------------------------------------------------------- |
| `product_to_add` | Product | The product to add for free                                     |
| `add_on_order`   | Text    | The order number that should include the free product, e.g. `3` |
| `quantity`       | Text    | Optional quantity, e.g. `1`                                     |

```javascript theme={null}
const params = input.params_normalized

const productId = params.product_to_add
const addOnOrder = Number.parseInt(params.add_on_order || "", 10)
const quantity = Number.parseInt(params.quantity || "1", 10)
const confirmedOrdersCount = input.confirmed_orders_count
const orderToPrepare = confirmedOrdersCount + 1

if (!productId) {
  return [
    {
      function: "no_action",
      arguments: {
        reason: "No product_to_add configured."
      }
    }
  ]
}

if (!Number.isFinite(addOnOrder) || addOnOrder < 2) {
  return [
    {
      function: "no_action",
      arguments: {
        reason: `Invalid add_on_order value: ${params.add_on_order}`
      }
    }
  ]
}

if (orderToPrepare === addOnOrder) {
  return [
    {
      function: "add_gift_to_subscription",
      arguments: {
        product_id: productId,
        quantity: String(Number.isFinite(quantity) && quantity > 0 ? quantity : 1),
        reason: `Add configured free one-off product so it ships with order ${addOnOrder}.`
      }
    }
  ]
}

return [
  {
    function: "no_action",
    arguments: {
      reason: `No configured free product due for order ${orderToPrepare}.`
    }
  }
]
```

### Product sequence with revolving cycle

Some subscription businesses ship different products depending on where the subscriber is in their journey. For example, a health supplements brand might send a starter kit on the first order, then alternate between two different refill packs from order 2 onward. After the first checkout order and possible follow-up orders, you can configure a revolving cycle that spans multiple orders.

This script uses **Input Parameters** to configure which products belong to each order in the sequence. You do not need to edit the JavaScript — just set the parameters in the workflow form.

#### Real-world example

Imagine a supplements brand that works like this:

* The customer checks out with a **Starter Kit** (order 1).
* On order 2, the subscription switches to two refill products: **Refill Pack A** and **Refill Pack B**.
* On order 3, the subscription has only **Refill Pack A**.
* From order 4 onward, orders 2 and 3 keep repeating: the customer alternates between receiving both refill packs and just one.

To set this up, you configure the Input Parameters like this:

| Key                    | Type    | Value         |
| ---------------------- | ------- | ------------- |
| `order_1`              | Product | Starter Kit   |
| `order_2`              | Product | Refill Pack A |
| `order_2`              | Product | Refill Pack B |
| `order_3`              | Product | Refill Pack A |
| `restart_from_order_2` | Text    | `true`        |

The resulting order sequence:

* Order 1 → Starter Kit (checkout order — already on subscription at signup)
* Order 2 → Refill Pack A + Refill Pack B (removes Starter Kit, adds both refills)
* Order 3 → Refill Pack A (removes Refill Pack B, keeps Refill Pack A)
* Order 4 → Refill Pack A + Refill Pack B (cycles back to order 2)
* Order 5 → Refill Pack A (cycles back to order 3)
* ...and so on

#### How the Input Parameters work

Each key follows the `order_X` format, where `X` is the order number. Set the type to **Product** and search for the product you want.

To add **multiple products to the same order**, add multiple rows with the same key (e.g. two rows both named `order_2`, each with a different product). The script collects all products for that order automatically.

`order_1` represents the **checkout order** — the products the customer receives at signup. These are already on the subscription, so the script does not add them. It uses `order_1` to know which products to remove when moving to `order_2`.

Add a key named `restart_from_order_X` (e.g. `restart_from_order_2`) with any value (e.g. `true`) to set where the revolving cycle begins. Once all configured orders have been fulfilled, the sequence loops back to this order number and repeats indefinitely.

#### The script

Paste this script into the Script Execution workflow. It reads the Input Parameters and handles add/remove automatically.

```javascript theme={null}
const params = input.params_normalized
const orderCount = input.confirmed_orders_count
const currentProducts = input.current_products || []

// Collect order_X keys and parse step numbers
const stepNumbers = Object.keys(params)
  .filter(k => k.startsWith("order_"))
  .map(k => parseInt(k.split("_")[1]))
  .filter(n => !isNaN(n))
  .sort((a, b) => a - b)
// Deduplicate step numbers (same key can appear in multiple rows)
const uniqueSteps = [...new Set(stepNumbers)]

if (uniqueSteps.length === 0) {
  return [{ function: "no_action", arguments: { reason: "No order_X steps configured in input params" } }]
}

const maxStep = uniqueSteps[uniqueSteps.length - 1]

// Find restart_from_order_X key to determine revolving cycle start
let alternateFrom = uniqueSteps[0]
for (const key of Object.keys(params)) {
  const match = key.match(/^restart_from_order_(\d+)$/)
  if (match) {
    alternateFrom = parseInt(match[1])
    break
  }
}

// The script runs after each order is confirmed.
// We prepare the subscription for the NEXT order.
// order_1 is the checkout order — its products are already on the subscription at signup.
// After order 1 is confirmed (orderCount=1), we swap to order_2 products, etc.
if (orderCount < 1) {
  return [{ function: "no_action", arguments: { reason: "No confirmed orders yet" } }]
}

const nextOrder = orderCount + 1

// Determine which step to prepare for
let effectiveStep
if (nextOrder <= maxStep) {
  effectiveStep = nextOrder
} else {
  // Revolving cycle: loop from alternateFrom to maxStep
  const cycleLength = maxStep - alternateFrom + 1
  const offset = (nextOrder - alternateFrom) % cycleLength
  effectiveStep = alternateFrom + offset
}

// Get the target product IDs for this step
// Supports both single values and arrays (from duplicate keys)
const stepKey = "order_" + effectiveStep
const rawTarget = params[stepKey]
const targetProductIds = Array.isArray(rawTarget) ? rawTarget : (rawTarget ? [rawTarget] : [])

if (targetProductIds.length === 0) {
  return [{ function: "no_action", arguments: { reason: "No products defined for step " + effectiveStep } }]
}

const currentProductIds = currentProducts.map(p => p.id)
const actions = []

// Remove products that should no longer be on the subscription
for (const currentId of currentProductIds) {
  if (!targetProductIds.includes(currentId)) {
    actions.push({
      function: "remove_product_from_subscription",
      arguments: {
        product_id: currentId,
        reason: "Removing product not in order_" + effectiveStep + " of sequence"
      }
    })
  }
}

// Add products that should be on the subscription but are not yet
for (const targetId of targetProductIds) {
  if (!currentProductIds.includes(targetId)) {
    actions.push({
      function: "add_product_to_subscription",
      arguments: {
        product_id: targetId,
        reason: "Adding product for order_" + effectiveStep + " of sequence"
      }
    })
  }
}

if (actions.length === 0) {
  return [{ function: "no_action", arguments: { reason: "Products already match order_" + effectiveStep } }]
}

return actions
```

## Supported functions

### `no_action`

Use this when the Journey should continue without making any subscription change right now.

Required arguments:

* `reason` (`string`)

Example:

```javascript theme={null}
return [
  {
    function: "no_action",
    arguments: {
      reason: "The subscription already matches the expected configuration."
    }
  }
]
```

### `swap_product_by_id`

Use this to replace one product on the subscription with another product.

Required arguments:

* `current_product_id` (`string`)
* `new_product_id` (`string`)
* `reason` (`string`)

Both product IDs must be Firmhouse product Global IDs.

Use the `firmhouse` namespace in these Global IDs, for example `gid://firmhouse/Product/123`. Do not use `gid://gomonthly/...`, because those IDs will not work in Script Execution workflows.

Example:

```javascript theme={null}
const currentProduct = input.current_products[0]

if (!currentProduct) {
  return [
    {
      function: "no_action",
      arguments: { reason: "No current product found." }
    }
  ]
}

return [
  {
    function: "swap_product_by_id",
    arguments: {
      current_product_id: currentProduct.id,
      new_product_id: "gid://firmhouse/Product/123",
      reason: "Move the subscriber to the recurring refill product."
    }
  }
]
```

### `update_next_billing_date`

Use this to move the subscription's next billing date.

To also move an existing scheduled order's shipment date, use [`reschedule_scheduled_order`](#reschedule_scheduled_order) as a separate action.

Required arguments:

* `next_billing_date` (`string`, ISO date format `YYYY-MM-DD`)
* `reason` (`string`)

Example:

```javascript theme={null}
return [
  {
    function: "update_next_billing_date",
    arguments: {
      next_billing_date: "2026-05-15",
      reason: "Align billing with the customer’s requested charge date."
    }
  }
]
```

### `update_plan_commitment_dates`

Use this to update the subscription plan's commitment dates. This is useful when a workflow needs to set, extend, or clear the plan's minimum commitment, maximum commitment, or grace cancellation dates.

Required arguments:

* `reason` (`string`)

Optional arguments:

* `minimum_commitment_ends_at` (`string`, ISO date or timestamp)
* `maximum_commitment_ends_at` (`string`, ISO date or timestamp)
* `grace_cancellation_ends_at` (`string`, ISO date or timestamp)

Pass an empty string for a date argument to clear that date.

Example:

```javascript theme={null}
return [
  {
    function: "update_plan_commitment_dates",
    arguments: {
      minimum_commitment_ends_at: "2026-12-31",
      maximum_commitment_ends_at: "2026-12-31",
      reason: "Align commitment dates with the renewed 6-month cycle."
    }
  }
]
```

### `apply_promotion`

Use this to apply a promotion to the subscription.

Required arguments:

* `promotion_id` (`string`)
* `reason` (`string`)

Example:

```javascript theme={null}
return [
  {
    function: "apply_promotion",
    arguments: {
      promotion_id: "123",
      reason: "Apply the onboarding discount after activation."
    }
  }
]
```

### `deactivate_applied_promotion`

Use this to deactivate a specific promotion already applied to the subscription, for example when a customer no longer qualifies for a discount. This action requires subscription context and is not available in project-level or scheduled runs.

Required arguments:

* `applied_promotion_id` (`integer` or `string`) — the ID or GlobalID of the subscription's applied promotion record
* `reason` (`string`)

Use the **applied promotion ID**, not the `promotion_id` used by `apply_promotion`. The applied promotion must belong to the subscription running the workflow. An already-inactive applied promotion is left unchanged.

Example (replace `456` with the applied promotion ID):

```javascript theme={null}
return [
  {
    function: "deactivate_applied_promotion",
    arguments: {
      applied_promotion_id: 456,
      reason: "The customer no longer qualifies for this discount."
    }
  }
]
```

You can also pass a numeric string such as `"456"`, or the applied promotion's GlobalID as returned by Firmhouse. A missing record, a record belonging to another subscription, or an invalid identifier causes the action to fail.

### `add_product_to_subscription`

Use this to add a product to the subscription.

Required arguments:

* `product_id` (`string`) — Firmhouse product Global ID
* `reason` (`string`)

Optional arguments:

* `quantity` (`string`) — defaults to `"1"` if not provided

Example:

```javascript theme={null}
return [
  {
    function: "add_product_to_subscription",
    arguments: {
      product_id: "gid://firmhouse/Product/123",
      reason: "Add the refill product for this cycle."
    }
  }
]
```

### `add_gift_to_subscription`

Use this to add a free one-off product to the subscription. The product is added for the next order only and is removed from the subscription after that order is created.

Required arguments:

* `product_id` (`string`) — Firmhouse product Global ID
* `reason` (`string`)

Optional arguments:

* `quantity` (`string`) — defaults to `"1"` if not provided

Example:

```javascript theme={null}
return [
  {
    function: "add_gift_to_subscription",
    arguments: {
      product_id: "gid://firmhouse/Product/123",
      quantity: "1",
      reason: "Add the loyalty present for the next order."
    }
  }
]
```

### `add_one_off_product_to_subscription`

Use this to add a one-off product to the subscription with an optional custom price. The product is added for the next order only and is removed from the subscription after that order is created.

Required arguments:

* `product_id` (`string`) — Firmhouse product Global ID
* `reason` (`string`)

Optional arguments:

* `quantity` (`string`) — defaults to `"1"` if not provided
* `custom_price_cents` (`string` or `number`) — custom price in cents. Use `0` for a free product.
* `skip_billing_product_replacement` (`boolean`) — set to `true` when the subscription's plan uses a Shopify billing product, but this one-off product should use its own Shopify variant instead of the billing product variant.

Example:

```javascript theme={null}
return [
  {
    function: "add_one_off_product_to_subscription",
    arguments: {
      product_id: "gid://firmhouse/Product/123",
      quantity: "1",
      custom_price_cents: 500,
      skip_billing_product_replacement: true,
      reason: "Add a discounted sample for the next order."
    }
  }
]
```

### `remove_product_from_subscription`

Use this to remove a product from the subscription.

Required arguments:

* `product_id` (`string`) — Firmhouse product Global ID
* `reason` (`string`)

Example:

```javascript theme={null}
return [
  {
    function: "remove_product_from_subscription",
    arguments: {
      product_id: "gid://firmhouse/Product/123",
      reason: "Remove the starter product after the bootstrap phase."
    }
  }
]
```

### `update_ordered_product_metadata`

Use this to replace the metadata object on an active ordered product. This is useful when a workflow needs to store data on the subscribed item itself, such as fulfillment metadata, external references, or configuration copied from an order event.

Required arguments:

* `ordered_product_id` (`string` or `integer`) — Firmhouse ordered product ID or Firmhouse ordered product Global ID
* `metadata` (`object`) — metadata to save on the ordered product
* `reason` (`string`)

The metadata object replaces the ordered product's existing metadata. Include any existing keys you want to keep.

Example: increment how many times each ordered product has been shipped. This assumes your webhook template includes each line's current ordered product metadata as `ordered_product_metadata`.

```javascript theme={null}
const orderLines = input.trigger.payload.order_lines || []

return orderLines
  .filter((line) => line.ordered_product_id)
  .map((line) => {
    const metadata = line.ordered_product_metadata || {}
    const timesShipped = Number(metadata.times_shipped || 0)

    return {
      function: "update_ordered_product_metadata",
      arguments: {
        ordered_product_id: line.ordered_product_id,
        metadata: {
          ...metadata,
          times_shipped: timesShipped + 1
        },
        reason: "Increment the shipped count on the ordered product."
      }
    }
  })
```

### `confirm_order`

Use this to confirm an order that is awaiting confirmation. This is useful when a Script Execution workflow checks the order and decides it can continue.

Required arguments:

* `order_id` (`string` or `integer`) — Firmhouse order ID or Firmhouse order Global ID for the awaiting confirmation order
* `reason` (`string`)

Example:

```javascript theme={null}
return [
  {
    function: "confirm_order",
    arguments: {
      order_id: input.trigger.payload.order_id,
      reason: "The order passed the configured checks."
    }
  }
]
```

### `skip_order`

Use this to skip an order that is awaiting confirmation.

Required arguments:

* `order_id` (`string` or `integer`) — Firmhouse order ID or Firmhouse order Global ID for the awaiting confirmation order
* `reason` (`string`)

Example:

```javascript theme={null}
return [
  {
    function: "skip_order",
    arguments: {
      order_id: input.trigger.payload.order_id,
      reason: "The order should not be created for this cycle."
    }
  }
]
```

### `schedule_order`

Use this to move a confirmed order back to the scheduled state with a future shipment date. For example, an **Order confirmed** outgoing webhook can trigger a Script Execution workflow that delays fulfillment until a date chosen by your business rules.

Required arguments:

* `order_id` (`string` or `integer`) — Firmhouse order ID or Firmhouse order Global ID for a confirmed order on the current subscription
* `shipment_date` (`string`) — future shipment date in `YYYY-MM-DD` format
* `reason` (`string`)

The action only changes confirmed orders. Running it again after the order is already scheduled for the same date has no additional effect. If the order has a Shopify fulfillment order, Firmhouse also requests that Shopify use the new fulfillment date.

For an **Order confirmed** webhook, add an order ID to the webhook template:

```json theme={null}
{
  "order_id": {{ order.id }}
}
```

The following example reads the shipment date from a Text input parameter named `shipment_date`:

```javascript theme={null}
return [
  {
    function: "schedule_order",
    arguments: {
      order_id: input.trigger.payload.order_id,
      shipment_date: input.params_normalized.shipment_date,
      reason: "Delay fulfillment until the configured shipment date."
    }
  }
]
```

### `reschedule_scheduled_order`

Use this to move a future scheduled order to an earlier or later shipment date. Both the current and new shipment dates must be in the future. Orders due today or overdue cannot be moved with this action because they may already be queued for processing. The order stays scheduled, and the subscription's next billing date is unchanged.

Required arguments:

* `order_id` (`string` or `integer`) — Firmhouse order ID or Firmhouse order Global ID for a scheduled order on the current subscription
* `shipment_date` (`string`) — future shipment date in `YYYY-MM-DD` format
* `reason` (`string`)

If the order has a linked Shopify fulfillment order, Firmhouse also requests a fulfillment-date update in Shopify. Repeating the action with the same shipment date leaves the stored date unchanged and retries Shopify synchronization.

The following example reads the scheduled order ID from the trigger payload and the target date from a Text input parameter named `shipment_date`. Configure the trigger payload to provide the ID of the scheduled order you want to move.

```javascript theme={null}
return [
  {
    function: "reschedule_scheduled_order",
    arguments: {
      order_id: input.trigger.payload.order_id,
      shipment_date: input.params_normalized.shipment_date,
      reason: "Move the scheduled order to the requested delivery date."
    }
  }
]
```

Use [`schedule_order`](#schedule_order) when the order is confirmed and needs to return to the scheduled state. Use [`update_next_billing_date`](#update_next_billing_date) separately when the subscription's billing date also needs to change.

If the action fails, check that the order belongs to the current subscription, is still scheduled for a future date, and that the requested date is a future date in `YYYY-MM-DD` format. When retrying, reuse the intended target date so the order is not moved by another interval.

### `add_order_line`

Use this to add a product line to an order that is awaiting confirmation.

Required arguments:

* `order_id` (`string` or `integer`) — Firmhouse order ID or Firmhouse order Global ID for the awaiting confirmation order
* `product_id` (`string` or `integer`) — Firmhouse product ID or Firmhouse product Global ID
* `reason` (`string`)

Optional arguments:

* `quantity` (`string` or `integer`) — defaults to `1` if not provided. Must be `1` or greater.
* `price_cents` (`string` or `integer`) — optional unit price in cents. Defaults to the product price when not provided. Use `0` for a free line.

Example:

```javascript theme={null}
return [
  {
    function: "add_order_line",
    arguments: {
      order_id: input.trigger.payload.order_id,
      product_id: "gid://firmhouse/Product/123",
      quantity: 1,
      price_cents: 0,
      reason: "Add a free sample to this order."
    }
  },
  {
    function: "confirm_order",
    arguments: {
      order_id: input.trigger.payload.order_id,
      reason: "The order is ready after adding the sample."
    }
  }
]
```

### `remove_order_line`

Use this to remove a product line from an order that is awaiting confirmation. The line is selected by the ordered product ID behind that order line.

Required arguments:

* `order_id` (`string` or `integer`) — Firmhouse order ID or Firmhouse order Global ID for the awaiting confirmation order
* `ordered_product_id` (`string` or `integer`) — Firmhouse ordered product ID or Firmhouse ordered product Global ID for the order line to remove
* `reason` (`string`)

Example:

```javascript theme={null}
return [
  {
    function: "remove_order_line",
    arguments: {
      order_id: input.trigger.payload.order_id,
      ordered_product_id: "gid://firmhouse/OrderedProduct/456",
      reason: "Remove the product from this order because the customer is not eligible."
    }
  },
  {
    function: "confirm_order",
    arguments: {
      order_id: input.trigger.payload.order_id,
      reason: "The order is ready after removing the ineligible product."
    }
  }
]
```

### `replace_order_line`

Use this to replace a product line on an order that is awaiting confirmation. This adds the replacement line before removing the original line, so the order is not left temporarily empty.

Required arguments:

* `order_id` (`string` or `integer`) — Firmhouse order ID or Firmhouse order Global ID for the awaiting confirmation order
* `ordered_product_id` (`string` or `integer`) — Firmhouse ordered product ID or Firmhouse ordered product Global ID for the order line to replace
* `replacement_product_id` (`string` or `integer`) — Firmhouse product ID or Firmhouse product Global ID for the replacement product
* `reason` (`string`)

Optional arguments:

* `quantity` (`string` or `integer`) — defaults to `1` if not provided. Must be `1` or greater.
* `price_cents` (`string` or `integer`) — optional unit price in cents. Defaults to the replacement product price when not provided. Use `0` for a free line.

Example:

```javascript theme={null}
return [
  {
    function: "replace_order_line",
    arguments: {
      order_id: input.trigger.payload.order_id,
      ordered_product_id: "gid://firmhouse/OrderedProduct/456",
      replacement_product_id: "gid://firmhouse/Product/789",
      quantity: 1,
      reason: "Replace the product with the customer's configured alternative."
    }
  },
  {
    function: "confirm_order",
    arguments: {
      order_id: input.trigger.payload.order_id,
      reason: "The order is ready after replacing the product."
    }
  }
]
```

### `stop_journey`

Use this when the Journey has reached its end and should not continue for this subscription.

Required arguments:

* `stop_journey` (`boolean`)
* `reason` (`string`)

Example:

```javascript theme={null}
return [
  {
    function: "stop_journey",
    arguments: {
      stop_journey: true,
      reason: "The workflow has completed all follow-up actions."
    }
  }
]
```

## What `input` contains

Firmhouse passes context into your script as `input`. The available fields depend on whether a webhook event or a schedule started the run.

### Event-triggered input

For an event-triggered run, Firmhouse passes the current subscription and outgoing webhook context. Most subscription fields follow a fixed structure. The webhook trigger context tells your script which event started the run and includes the rendered webhook payload.

The current `input` payload contains:

* `signed_up_at`
* `confirmed_orders_count`
* `confirmed_or_fulfilled_orders`
* `current_products`
* `current_promotions`
* `active_plan`
* `trigger` — outgoing webhook context for the event that started the script. `input.trigger.payload` contains the rendered webhook template, so its fields depend on what you include in that template.
* `params` — the raw array of Input Parameter objects you configured on the workflow (empty array `[]` when none are set). Each entry has `key`, `type`, and `value`.
* `params_normalized` — a convenience hash that groups `params` by key. Duplicate keys become arrays. Empty object `{}` when no parameters are set.

To start follow-up logic for a specific accepted churn-prevention offer, select the **Promotional offer accepted** event and include the `promotional_offer` and `churn_request` fields you need in the outgoing webhook template. The rendered values are available under `input.trigger.payload`. This event is offer-specific; the broader **Churn request prevented** event also fires for pause, skip, and legacy discount winbacks.

Example:

```json theme={null}
{
  "signed_up_at": "2026-04-01T10:15:00Z",
  "confirmed_orders_count": 1,
  "confirmed_or_fulfilled_orders": [
    {
      "id": "gid://firmhouse/Order/1",
      "order_lines": [
        {
          "id": "gid://firmhouse/OrderLine/1",
          "product_id": 123,
          "product_sku": "STARTER-001",
          "quantity": 1
        }
      ]
    }
  ],
  "current_products": [
    {
      "id": "gid://firmhouse/Product/123",
      "product_id": 123,
      "title": "Starter Product",
      "sku": "STARTER-001",
      "quantity": 1,
      "instalment_original_product_id": 123,
      "instalment_current_number": 1
    }
  ],
  "current_promotions": [
    {
      "created_at": "2026-04-01T10:15:00Z",
      "id": "gid://firmhouse/AppliedPromotion/1",
      "promotion_id": "gid://firmhouse/Promotion/1",
      "title": "Welcome Discount",
      "active": "true"
    }
  ],
  "active_plan": {
    "subscribed_plan": {
      "id": "gid://firmhouse/SubscribedPlan/1",
      "next_billing_date": "2026-04-15",
      "billing_cycle_interval_period": 1,
      "billing_cycle_interval_unit": "months",
      "delivery_cycle_interval_period": 1,
      "delivery_cycle_interval_unit": "months",
      "cancellation_strategy": "immediate"
    },
    "plan": {
      "id": "gid://firmhouse/Plan/1",
      "name": "Starter Plan",
      "slug": "starter-plan",
      "instalments": 3,
      "billing_cycle_interval_period": 1,
      "billing_cycle_interval_unit": "months",
      "delivery_cycle_interval_period": 1,
      "delivery_cycle_interval_unit": "months"
    }
  },
  "trigger": {
    "source": "outgoing_webhook",
    "event": "order_pre_confirmation",
    "outgoing_webhook_id": 42,
    "referenceable_gid": "gid://firmhouse/Order/123",
    "payload": {}
  }
}
```

#### Field details

* `signed_up_at`
  The subscription sign-up timestamp.
* `confirmed_orders_count`
  The number of confirmed or fulfilled orders on the subscription.
* `confirmed_or_fulfilled_orders`
  The confirmed or fulfilled orders, including their order lines.
* `current_products`
  The currently active products on the subscription, including instalment-related fields:
  `product_id`, `instalment_original_product_id`, and `instalment_current_number`.
* `current_promotions`
  The promotions currently applied to the subscription.
* `active_plan`
  Selected plan details for the subscription, including both `subscribed_plan` attributes and full `plan` attributes.
* `trigger`
  Outgoing webhook context for the event that started the script. This includes `source`, `event`, `outgoing_webhook_id`, and `payload`. If the event is related to a specific record, it can also include `referenceable_gid`. The payload shape depends on the webhook template. The order pre-confirmation examples on this page assume your template includes an `order_id` field, which is then available as `input.trigger.payload.order_id`.

You can inspect the `input` object in your script and use the fields you need in your logic.

`input.params` contains the raw array of parameter objects you defined in the **Input Parameters** section of the workflow form. `input.params_normalized` provides the same data grouped by key for convenient lookups. See [Input Parameters](#input-parameters) for details on how to configure them.

### Scheduled input

For a scheduled run, Firmhouse passes project context instead of subscription context. `input.trigger.payload` is an empty object because no external webhook payload starts the run.

```json theme={null}
{
  "project": {
    "id": "gid://firmhouse/Project/123",
    "name": "Example project"
  },
  "params": [],
  "params_normalized": {},
  "trigger": {
    "source": "schedule",
    "event": "scheduled",
    "outgoing_webhook_id": 42,
    "payload": {},
    "scheduled_for": "2026-07-29T12:00:00+02:00"
  }
}
```

The scheduled input contains:

* `project.id` — the project's Global ID
* `project.name` — the project name
* `params` and `params_normalized` — the workflow's configured Input Parameters
* `trigger.source` — always `schedule`
* `trigger.event` — always `scheduled`
* `trigger.outgoing_webhook_id` — the outgoing webhook that owns the schedule
* `trigger.payload` — an empty object
* `trigger.scheduled_for` — the intended execution time, including the Amsterdam UTC offset

## Investigate an execution

The **Automation Activity Log** preview must be enabled for your project. Ask Firmhouse staff to enable it under **New Features**.

1. Open **Workflows → Activity log**. If your project also has Workflow Builder, the legacy script workflows are under **Automations → Activity log**.
2. Search by customer name, email, or subscription ID.
3. Open **Filters** to narrow the results by automation, status, or start date. Results update automatically. The initial view covers today and the previous six days. You can choose earlier dates to find older retained records.
4. Select **View details** for a Journey activity.

Activity details show the available trigger, subscription, recorded input, console output, errors, total execution time, and returned actions. A script activity contains its recorded diagnostics. Individual Journey actions appear as separate activities with their own status. A script can return successfully while one of its actions fails.

Each newly recorded script run has its own activity, including runs started by existing automatic or manual retries. Output emitted before a script failure remains available when the execution provider returned it. Total execution time includes work performed before failure. Timing messages emitted by your script remain visible in console output.

The Type column distinguishes scripts, historical decisions, and individual action names. The list shows all history by default, newest first, with pagination. Counts reflect the active filters.

<Note>
  Older Journey activities may have less diagnostic information. Logs, timing measurements, and build logs that were never stored cannot be recovered. Details show the information available for each activity.
</Note>

Access follows your project's permissions. Secret input parameter values and configured sensitive fields are filtered from the diagnostics. The existing Journey overview remains available.

## Troubleshooting

If the script does not save or does not run correctly, check the following:

* the script contains valid JavaScript
* the script returns an array
* every action includes `function` and `arguments`
* all required arguments are present
* there are no extra unsupported arguments
* the argument types match the required types
* scheduled scripts only return project-level actions
* the scheduled outgoing webhook is enabled

If the workflow still fails, contact Firmhouse support and include:

* your project ID
* the name of the workflow
* the script you saved
* the action you expected the script to return

## Related articles

* [Post-checkout Product Swap](/configure/workflows/post-checkout-product-swap)
