# Write a workflow in JSON

> A hands-on tutorial that builds a find-or-create workflow as JSON, one key at a time, then creates, tests, enables and updates it with the Workflows API and kusty. Covers root keys, triggers, steps and params, transitions, error cases, variables by ID, partial updates, and the validation errors you'll meet.

Source: https://developer.kustomer.com/docs/workflows/write-workflow-json

Every workflow is a JSON document. The builder writes that document for you, but you can also write it yourself, keep it in source control, review changes in pull requests, and ship it with one API call. This tutorial builds a real workflow that way, starting from an empty skeleton.

You'll build **Record shipped orders**. Your store posts to a Kustomer [web hook](https://developer.kustomer.com/docs/events/inbound-webhooks) each time an order ships. The workflow looks up the customer by email, creates the customer if there isn't one, and records the order as a custom object on that customer.

Before you start, you need:

- An API key with the `org.admin.workflow.write` role (or `org.permission.workflow.create` and `org.permission.workflow.update`) and the matching read role. See [API keys and roles](https://developer.kustomer.com/docs/get-started/api-keys-and-roles).
- A web hook for the store. [Create one](https://developer.kustomer.com/docs/events/inbound-webhooks#create-a-hook) and keep its `eventName`.
- Optionally, [kusty](https://developer.kustomer.com/docs/ai/kusty), the Kustomer CLI. Every request below is shown with cURL and kusty.

If you haven't read how a run moves through a workflow, start with [How workflows run](https://developer.kustomer.com/docs/workflows/how-workflows-run) and [Data and variables](https://developer.kustomer.com/docs/workflows/data-and-variables). To build the same kind of workflow by clicking instead of typing, see [Build in the builder](https://developer.kustomer.com/docs/workflows/build-in-the-builder).

_An interactive stepper that grows the record-shipped-orders workflow JSON through six stages, with the lines each stage adds highlighted and a flow diagram that gains the same steps. Skeleton: only name, description, enabled false, meta.displayName and an empty steps array. Trigger: a web hook trigger with id 1 and no transitions. First step: a find-customer step using kustomer.customer.find-by-email, reached from the trigger when the payload has both an email and an order number. Lookup: a record-order step that reads /#steps.find-customer.id, reached when that ID exists. Branches: a second transition, taken when the ID does not exist, leads to create-customer and then to its own record-order-new-customer step. Error case: create-customer gets an error case that stops the run quietly when /#error.status is 400. The final stage is exactly the finished workflow shown later on this page._

## Start from a skeleton

Create a file called `record-shipped-orders.json` with the root keys:

```json title="record-shipped-orders.json"
{
  "name": "record-shipped-orders",
  "description": "Records shipped orders from the store's web hook, creating the customer if needed.",
  "enabled": false,
  "meta": { "displayName": "Record shipped orders" },
  "steps": []
}
```

This is already a document the API accepts. It just doesn't do anything yet.

| Key                | Rules                                                                                                                                                                                                                                                                                                           |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`             | Required. Letters, numbers, `-`, `_` and `.`, up to 128 characters, no spaces. Kustomer stores it lowercased and trimmed, and it must be unique in your org, so write it in lowercase yourself. The builder makes the name from the title the same way: `Record shipped orders` becomes `record-shipped-orders` |
| `description`      | Up to 4,096 characters                                                                                                                                                                                                                                                                                          |
| `enabled`          | New workflows are created disabled. Keep it `false` until you've tested                                                                                                                                                                                                                                         |
| `meta`             | Must be **flat**: up to 100 keys matching `^[a-zA-Z][a-zA-Z0-9-_]{1,64}$`, each holding a string (up to 1,024 characters), number, boolean or `null`. Nested objects are rejected. `displayName` is the title the builder shows, so it can have spaces and capitals                                             |
| `trigger`, `steps` | Added in the next sections                                                                                                                                                                                                                                                                                      |

The root accepts a few more keys, such as `replaces` and `logging`. Unknown keys are rejected. The [Workflow JSON reference](https://developer.kustomer.com/docs/workflows/workflow-json) lists them all, so this page only covers what you need.

## Add the trigger

The trigger names the event that starts a run. For a web hook it's the hook's `eventName`:

```json title="Add after meta"
  "trigger": {
    "id": "1",
    "eventName": "kustomer.hooks.web.5825c452be3af71000ac6462.receive",
    "transitions": []
  },
```

Replace `5825c452be3af71000ac6462` with your hook's ID. The trigger's `id` is always `"1"`, so its output is **step 1**: everything later in the workflow reads the event at `/#steps.1…`. For a web hook, step 1 is the hook transaction, and the fields your store posted are under `attributes.data`. When the store posts this:

```json title="What the store posts"
{ "orderNumber": "4417", "email": "jane@example.com", "name": "Jane Rivera", "total": "129.90" }
```

the email is at `/#steps.1.attributes.data.email` and the order number at `/#steps.1.attributes.data.orderNumber`. Record triggers, such as `kustomer.conversation.update`, flatten the record instead: see [What the trigger outputs](https://developer.kustomer.com/docs/workflows/triggers-and-actions#what-the-trigger-outputs).

`transitions` is empty for now, so a run would end right after the trigger. Transitions need a step to point at, which comes next.

## Add the first step

Steps go in the `steps` array. Add one that looks up the customer by the email in the payload, and point the trigger at it:

```json title="record-shipped-orders.json (trigger and steps)"
  "trigger": {
    "id": "1",
    "eventName": "kustomer.hooks.web.5825c452be3af71000ac6462.receive",
    "transitions": [
      {
        "target": "find-customer",
        "condition": {
          "op": "and",
          "values": [
            { "op": "exists", "values": ["/#steps.1.attributes.data.email"] },
            { "op": "exists", "values": ["/#steps.1.attributes.data.orderNumber"] }
          ]
        },
        "meta": { "name": "Has an email and order number" }
      }
    ]
  },
  "steps": [
    {
      "id": "find-customer",
      "action": "kustomer.customer.find-by-email",
      "params": { "email": "/#steps.1.attributes.data.email" },
      "meta": { "displayName": "Find the customer" },
      "transitions": []
    }
  ]
```

The step has four parts:

- **`id`** is how the rest of the workflow refers to this step: its output will be stored at `/#steps.find-customer`. The builder generates random IDs such as `SAk4K54Ly`; in JSON, pick readable ones. Use letters, numbers, `-` and `_`, and no dots, because transition targets can't contain dots.
- **`action`** is what the step does. Action names and their required inputs are in [Triggers and actions](https://developer.kustomer.com/docs/workflows/triggers-and-actions#built-in-actions). In the builder, this one is **Customer: Lookup (By Email)**.
- **`params`** are the action's inputs. `"/#steps.1.attributes.data.email"` is a **reference**: because the whole string starts with `/#`, it resolves to the value at that path and keeps its type. Any other string is a Handlebars template and always produces text. [Data and variables](https://developer.kustomer.com/docs/workflows/data-and-variables) covers references, templates and functions in depth.
- **`meta.displayName`** is the step's title in the builder.

The trigger's transition has a **target** (a step ID) and a **condition**. This condition is an `and` group, so the run only reaches `find-customer` when the payload has both an email and an order number. If no transition matches, the run ends successfully. That's how most runs end, so a strict first condition is the cheapest filter you have. `meta.name` labels the branch in the builder; name every condition so the canvas reads well.

## Use the lookup's output

A step's output is stored under its ID, so the next step can use the customer the lookup found. Add a step that records the order on that customer, and a transition from the lookup to it:

```json title="Add to find-customer's transitions"
        {
          "target": "record-order",
          "condition": { "op": "exists", "values": ["/#steps.find-customer.id"] },
          "meta": { "name": "Customer found" }
        }
```

```json title="Add to steps"
    {
      "id": "record-order",
      "action": "kustomer.kobject.create-with-customer",
      "params": {
        "klassName": "order",
        "customer": "/#steps.find-customer.id",
        "title": "Order {{{steps.1.attributes.data.orderNumber}}}",
        "data": {
          "orderNumberStr": "/#steps.1.attributes.data.orderNumber",
          "totalNum": "/#fn:parseFloat,steps.1.attributes.data.total"
        }
      },
      "meta": { "displayName": "Record the order" },
      "transitions": []
    }
```

Three kinds of values appear in these params:

| Value                                               | Kind      | Result                                                                      |
| --------------------------------------------------- | --------- | --------------------------------------------------------------------------- |
| `"/#steps.find-customer.id"`                        | Reference | The customer's ID, read from the lookup's output                            |
| `"Order {{{steps.1.attributes.data.orderNumber}}}"` | Template  | The text `Order 4417`. Triple braces insert the value without HTML escaping |
| `"/#fn:parseFloat,steps.1.attributes.data.total"`   | Function  | The number `129.9`. Function arguments are bare paths, without `/#`         |

The transition checks `exists` because **a lookup that finds nothing doesn't fail**. The API answers `404`, the step succeeds with empty output, and `/#steps.find-customer.id` is empty. Without the check, `record-order` would run with no customer.

`transitions: []` makes `record-order` a final step. The `order` Klass and its `orderNumberStr` and `totalNum` fields are examples: use a Klass and fields that exist in your org. See [Custom objects](https://developer.kustomer.com/docs/concepts/custom-objects).

## Branch: find or create

Right now a new customer's order is dropped: the lookup comes back empty, no transition matches, and the run ends. Add a second transition for that case, a step that creates the customer, and a step that records the order on the new customer:

```json title="find-customer's transitions"
      "transitions": [
        {
          "target": "record-order",
          "condition": { "op": "exists", "values": ["/#steps.find-customer.id"] },
          "meta": { "name": "Customer found" }
        },
        {
          "target": "create-customer",
          "condition": { "op": "dne", "values": ["/#steps.find-customer.id"] },
          "meta": { "name": "No customer yet" }
        }
      ]
```

```json title="Add to steps"
    {
      "id": "create-customer",
      "action": "kustomer.customer.create",
      "params": {
        "name": "/#steps.1.attributes.data.name",
        "email": "/#steps.1.attributes.data.email"
      },
      "meta": { "displayName": "Create the customer" },
      "transitions": [
        {
          "target": "record-order-new-customer",
          "condition": { "op": "exists", "values": ["/#steps.create-customer.id"] },
          "meta": { "name": "Customer created" }
        }
      ]
    },
    {
      "id": "record-order-new-customer",
      "action": "kustomer.kobject.create-with-customer",
      "params": {
        "klassName": "order",
        "customer": "/#steps.create-customer.id",
        "title": "Order {{{steps.1.attributes.data.orderNumber}}}",
        "data": {
          "orderNumberStr": "/#steps.1.attributes.data.orderNumber",
          "totalNum": "/#fn:parseFloat,steps.1.attributes.data.total"
        }
      },
      "meta": { "displayName": "Record the order for the new customer" },
      "transitions": []
    }
```

_The find-or-create shape. The web hook trigger (step 1) goes to find-customer when the payload has an email and an order number. From find-customer, the first transition goes to record-order when the customer was found, and the second goes to create-customer when there is no customer yet. create-customer goes to its own final step, record-order-new-customer, when the new customer's ID exists._

A few rules shape this:

- **Transitions are checked in order, and the first match wins.** `exists` and `dne` can't both pass here, so the order doesn't matter. When conditions can overlap, put the most specific first.
- **`dne`** ("does not exist") passes when the value is empty, `0`, `false` or missing. `exists` is its opposite. The full operator list is in [Conditions and variables](https://developer.kustomer.com/docs/workflows/conditions-and-expressions#operators).
- **Branches don't merge back.** Each branch ends in its own final step, which is why the order step appears twice with a different `customer`. Kustomer's own templates are built the same way.
- **Transitions can't loop back** to a step that's already on the path. The API rejects the save (see [Validation errors](#validation-errors-youll-hit)). To repeat work later, [schedule a callable workflow](https://developer.kustomer.com/docs/workflows/patterns#schedule-follow-ups-instead-of-looping).

## Handle errors

A Kustomer action that gets a `4xx` other than `404`, such as a validation error, **fails** the step. Without an error case, the run stops and the failure is recorded on the workflow. If the store sends an email Kustomer won't accept, `create-customer` fails that way. To stop quietly instead, add `errorCases` to the step, before its `transitions`:

```json title="Add to create-customer"
      "errorCases": [
        {
          "condition": {
            "op": "or",
            "values": [{ "op": "eq", "values": ["/#error.status", 400] }]
          },
          "errorAction": { "type": "stop-processing" }
        }
      ],
```

- While error cases are checked, the error is at **`/#error`**: the first item of the API's `errors` array, with `status`, `code`, `title`, `detail` and `source`. It's never at `/#steps.create-customer.error`.
- An error case's `condition.op` must be `and` or `or`, with up to five conditions inside. An empty `values` array matches every error. Cases are checked in order.
- `stop-processing` ends the run without recording a failure. `continue` moves on to the step's transitions with no output, which is why `create-customer`'s transition checks `exists`. Don't use `retry-workflow` or `retry-action`: neither retries.

Leave the error case out if you'd rather see every rejected payload in the workflow's error log.

> **Error cases are JSON-only:** The classic builder has no editor for error cases. Add them in JSON, either with the API as shown here or with **Edit workflow JSON** in the builder.

> **A 5xx restarts the run from step 1:** When a Kustomer action gets a `5xx`, error cases are skipped and Kustomer runs the workflow again **from step 1**. Steps that already succeeded run again, so a step that creates something can create it twice, and the retry isn't recorded in the error log. See [Make steps safe to repeat](https://developer.kustomer.com/docs/workflows/patterns#make-steps-safe-to-repeat) for ways to guard against that.

## The finished workflow

```json title="record-shipped-orders.json"
{
  "name": "record-shipped-orders",
  "description": "Records shipped orders from the store's web hook, creating the customer if needed.",
  "enabled": false,
  "meta": { "displayName": "Record shipped orders" },
  "trigger": {
    "id": "1",
    "eventName": "kustomer.hooks.web.5825c452be3af71000ac6462.receive",
    "transitions": [
      {
        "target": "find-customer",
        "condition": {
          "op": "and",
          "values": [
            { "op": "exists", "values": ["/#steps.1.attributes.data.email"] },
            { "op": "exists", "values": ["/#steps.1.attributes.data.orderNumber"] }
          ]
        },
        "meta": { "name": "Has an email and order number" }
      }
    ]
  },
  "steps": [
    {
      "id": "find-customer",
      "action": "kustomer.customer.find-by-email",
      "params": { "email": "/#steps.1.attributes.data.email" },
      "meta": { "displayName": "Find the customer" },
      "transitions": [
        {
          "target": "record-order",
          "condition": { "op": "exists", "values": ["/#steps.find-customer.id"] },
          "meta": { "name": "Customer found" }
        },
        {
          "target": "create-customer",
          "condition": { "op": "dne", "values": ["/#steps.find-customer.id"] },
          "meta": { "name": "No customer yet" }
        }
      ]
    },
    {
      "id": "record-order",
      "action": "kustomer.kobject.create-with-customer",
      "params": {
        "klassName": "order",
        "customer": "/#steps.find-customer.id",
        "title": "Order {{{steps.1.attributes.data.orderNumber}}}",
        "data": {
          "orderNumberStr": "/#steps.1.attributes.data.orderNumber",
          "totalNum": "/#fn:parseFloat,steps.1.attributes.data.total"
        }
      },
      "meta": { "displayName": "Record the order" },
      "transitions": []
    },
    {
      "id": "create-customer",
      "action": "kustomer.customer.create",
      "params": {
        "name": "/#steps.1.attributes.data.name",
        "email": "/#steps.1.attributes.data.email"
      },
      "meta": { "displayName": "Create the customer" },
      "errorCases": [
        {
          "condition": {
            "op": "or",
            "values": [{ "op": "eq", "values": ["/#error.status", 400] }]
          },
          "errorAction": { "type": "stop-processing" }
        }
      ],
      "transitions": [
        {
          "target": "record-order-new-customer",
          "condition": { "op": "exists", "values": ["/#steps.create-customer.id"] },
          "meta": { "name": "Customer created" }
        }
      ]
    },
    {
      "id": "record-order-new-customer",
      "action": "kustomer.kobject.create-with-customer",
      "params": {
        "klassName": "order",
        "customer": "/#steps.create-customer.id",
        "title": "Order {{{steps.1.attributes.data.orderNumber}}}",
        "data": {
          "orderNumberStr": "/#steps.1.attributes.data.orderNumber",
          "totalNum": "/#fn:parseFloat,steps.1.attributes.data.total"
        }
      },
      "meta": { "displayName": "Record the order for the new customer" },
      "transitions": []
    }
  ]
}
```

Kustomer adds an `appVersion`, such as `kustomer-^1.2.1`, to steps saved in the builder. It's optional when you write JSON.

## Create it

`POST /v1/workflows` ([reference](https://developer.kustomer.com/api/workflows/create-a-workflow))

Preview the request first, then send it:

```bash title="kusty"
kusty api POST /v1/workflows --data @record-shipped-orders.json --dry-run
kusty api POST /v1/workflows --data @record-shipped-orders.json --fields id,attributes.name,attributes.enabled --output json
```

```bash title="cURL"
curl -X POST "https://acme.api.kustomerapp.com/v1/workflows" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  --data @record-shipped-orders.json
```

```json title="201 Created (abridged)"
{
  "data": {
    "type": "workflow",
    "id": "6716c1d2c4e5f60012ab3700",
    "attributes": {
      "name": "record-shipped-orders",
      "enabled": false,
      "trigger": { "id": "1", "eventName": "kustomer.hooks.web.5825c452be3af71000ac6462.receive", "transitions": [ … ] },
      "steps": [ … ],
      "lastError": {}
    }
  }
}
```

Keep `data.id`. You'll use it to update the workflow, add variables and read its failures:

```bash
export WORKFLOW_ID=6716c1d2c4e5f60012ab3700
```

kusty checks the body against the API's JSON Schema before it sends anything, so a misspelled key or a nested `meta` fails locally with `SCHEMA_VALIDATION_FAILED` (exit code 2). Checks that need your org, such as unique names and the loop check, happen on the server. See [Validation errors](#validation-errors-youll-hit).

## Reference variables by ID

This workflow doesn't need a secret, but many do: for example, a [REST step](https://developer.kustomer.com/docs/workflows/code-steps) that calls your warehouse's API with a token. Store the token in a **workflow variable**, never in params. Variables belong to one workflow, so create them after the workflow exists:

```bash title="kusty"
kusty api POST /v1/workflows/$WORKFLOW_ID/variables \
  --data '{ "name": "warehouseToken", "value": "wh_live_…", "type": "string" }' \
  --fields id,attributes.name --output json
```

```bash title="cURL"
curl -X POST "https://acme.api.kustomerapp.com/v1/workflows/$WORKFLOW_ID/variables" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "warehouseToken", "value": "wh_live_…", "type": "string" }'
```

```json title="201 Created (abridged)"
{ "data": { "id": "6716c1d2c4e5f60012ab36f1", "type": "variable", "attributes": { "name": "warehouseToken", "maskedValue": "…", "type": "string" } } }
```

Reference the variable by its **ID**, `data.id`, not its name. As a whole param use `/#variables.<id>`, and inside a template use `{{{variables.<id>}}}`:

```json title="A REST step's headers"
{
  "headers": {
    "Content-Type": "application/json",
    "Authorization": "Bearer {{{variables.6716c1d2c4e5f60012ab36f1}}}"
  }
}
```

`{{{variables.warehouseToken}}}` resolves to nothing, and so does an ID from another workflow. Variable values are strings of up to 1,024 characters, and the API only ever returns the last six characters. Then add the step with a [`PUT`](#update-it), as described below.

## Turn it on and test

A disabled workflow doesn't run, so testing a hook workflow means enabling it. Test in a sandbox org if you can, because this workflow creates real customers and orders.

#### Enable it and turn on logging

Send only the keys you're changing:

```bash title="kusty"
kusty api PUT /v1/workflows/$WORKFLOW_ID --data '{ "enabled": true, "logging": true }'
```

```bash title="cURL"
curl -X PUT "https://acme.api.kustomerapp.com/v1/workflows/$WORKFLOW_ID" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" -H "Content-Type: application/json" \
  -d '{ "enabled": true, "logging": true }'
```

`logging: true` records every run for the next hour, the same as **Debug Mode** in the builder's **View Logs**.

#### Wait about a minute

Workflows are cached, so a created, enabled or changed workflow takes about a minute to reach new runs.

#### Post one payload per path

Send an order for an existing customer, one for a new email, and one without an `orderNumber`:

```bash
curl -X POST "$HOOK_URL" \
  -H "Authorization: Bearer $KUSTOMER_HOOK_KEY" -H "Content-Type: application/json" \
  -d '{ "orderNumber": "4417", "email": "jane@example.com", "name": "Jane Rivera", "total": "129.90" }'
```

#### Read the runs

Open the workflow in the builder and select **View Logs** to see each run's path and every step's input and output. The first should end at `record-order`, the second at `record-order-new-customer`, and the third right after the trigger. Failed runs are also in [`GET /v1/workflows/{id}/processes`](https://developer.kustomer.com/api/workflows/get-processes-errors-by-workflow). See [Debugging](https://developer.kustomer.com/docs/workflows/debugging).

## Update it

`PUT /v1/workflows/{id}` ([reference](https://developer.kustomer.com/api/workflows/update-a-workflow-based-on-the-id))

`PUT /v1/workflows/{id}` updates **only the root keys you send**. Each key you send replaces the stored value whole, and keys you leave out stay as they are. That's how the builder works too: its on/off switch sends only `enabled`, and **Save** sends `trigger` and `steps`.

| To change              | Send                                                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| On or off              | `{ "enabled": false }`                                                                                                   |
| The title              | The whole `meta` object, because it's replaced as a unit: `{ "meta": { "displayName": "Record shipped store orders" } }` |
| Any step or transition | `trigger` **and** `steps`, together                                                                                      |

Always send `trigger` and `steps` together. The trigger's transitions point at step IDs, so replacing one without the other can leave a transition aimed at a step that no longer exists, and the run fails with `next step not found`.

To change the logic, fetch the current document, keep only the keys you're changing, edit, preview and send:

```bash title="kusty"
kusty api GET /v1/workflows/$WORKFLOW_ID --output json \
  | jq '.data.attributes | {trigger, steps} | del(.steps[]._id)' > logic.json
# edit logic.json
kusty api PUT /v1/workflows/$WORKFLOW_ID --data @logic.json --dry-run
kusty api PUT /v1/workflows/$WORKFLOW_ID --data @logic.json
```

```bash title="cURL"
curl -s "https://acme.api.kustomerapp.com/v1/workflows/$WORKFLOW_ID" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  | jq '.data.attributes | {trigger, steps} | del(.steps[]._id)' > logic.json
# edit logic.json
curl -X PUT "https://acme.api.kustomerapp.com/v1/workflows/$WORKFLOW_ID" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" -H "Content-Type: application/json" \
  --data @logic.json
```

- Don't send back what the API adds: `lastError`, `editable`, `containsVariables`, the timestamps, and the `_id` on each stored step. Selecting keys with `jq`, as above, leaves them out.
- Keep step IDs stable. Every `/#steps.<id>` reference and every `target` depends on them. If you rename a step, update all of them.
- Changing `name` changes how the workflow is listed; look workflows up by `id`, which never changes.
- Kustomer keeps the 30 most recent versions of a workflow. The builder's version picker opens older ones read-only.
- Allow about a minute before testing the change.

## Validation errors you'll hit

The API validates every create and update. These are the errors you're most likely to see while writing JSON by hand:

| Status | Error mentions                                                                   | Cause and fix                                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | `Step with ID <id> transitions to a previous step with ID <workflowId>:<target>` | A transition points back to a step already on the path. Workflows can't loop. To repeat work later, [schedule a callable workflow](https://developer.kustomer.com/docs/workflows/patterns#schedule-follow-ups-instead-of-looping)                            |
| `400`  | `uniqueItems`                                                                    | Two identical transitions on one step. Remove the duplicate                                                                                                                                                                                                  |
| `409`  | the name                                                                         | Another workflow in the org already has this `name`. Names are compared after lowercasing, so `Record-Shipped-Orders` and `record-shipped-orders` clash. Pick a new name, or update the existing workflow instead                                            |
| `400`  | `additionalProperties`                                                           | An unknown key, a read-only attribute copied from a `GET`, or a nested object in `meta`                                                                                                                                                                      |
| `400`  | `target` pattern                                                                 | A step ID used as a target contains a dot                                                                                                                                                                                                                    |
| `400`  | `maxItems`                                                                       | More than 10 transitions or 10 error cases on one step, or more than 5 conditions in an error case                                                                                                                                                           |
| `400`  | `Invalid app settings configuration passed`                                      | A `{{{ setting … }}}` template refers to a setting that doesn't exist                                                                                                                                                                                        |
| `409`  |                                                                                  | The workflow was installed by an app and can't be edited. You can still turn it on or off and toggle logging. To change it, [copy it and use `replaces`](https://developer.kustomer.com/docs/workflows/workflow-json#override-an-app-workflow-with-replaces) |

Some mistakes pass validation and only show up at run time: a target that doesn't match any step ID (`next step not found`), a reference to a misspelled path (it silently resolves to nothing), or a variable referenced by name. Test every path, as described above.

## Next
