> For the complete documentation index, see [llms.txt](https://docs.pyck.cloud/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.pyck.cloud/shopify-app/schema-design.md).

# Designing pyck schemas for Shopify

How to design pyck schemas so the Shopify app can write into them, and how to name the fields you add

Audience: Programmer

If a pyck tenant will be connected to a Shopify store, now or later, design its schemas (DataTypes) for items, customers, picking orders and order lines around the way the Shopify app writes. The app puts everything it reads from Shopify into one object field, `shopify`. Every other field of the schema is yours: the app leaves it alone unless someone gives it a Shopify source in the app's Data mapping.

This page is for whoever designs the tenant's schemas, whether that is the customer's own developer or the pyck team.

{% hint style="info" %}
**The short version.** Let your schema accept `shopify` as an object. Read Shopify's data from `data.shopify` instead of declaring your own copy of it. Give warehouse-only data names of your own. For values held in Shopify metafields, name the pyck field after the metafield key.
{% endhint %}

## How the app writes a record

Every record the app writes has a `data` object with up to three kinds of content:

| Content                                               | Who writes it                | On every sync                                             |
| ----------------------------------------------------- | ---------------------------- | --------------------------------------------------------- |
| `data.shopify`                                        | The app                      | Replaced as a whole with what Shopify has now             |
| A field mapped to a Shopify source (Custom mode only) | The app                      | Overwritten; removed when the value is cleared in Shopify |
| Every other field                                     | Your warehouse and workflows | Never touched                                             |

Your own fields sit **next to** `shopify` at the top level of `data`, never inside it. A customer record in a tenant that maps `display_name` to the Shopify field **Display name** and keeps `storage_zone` for the warehouse looks like this:

```json
{
  "shopify": { "id": "gid://shopify/Customer/6987654321", "firstName": "Anna", "lastName": "Becker" },
  "display_name": "Anna Becker",
  "storage_zone": "B-12"
}
```

`data.shopify` is the same whatever you map. Mapping a field never moves a value out of it and never adds one to it: the mapped field is an additional copy, in the place and type you chose. A field without a source never receives anything from Shopify.

`data.shopify` holds the Shopify record as Shopify's Admin GraphQL API returns it:

* **Shopify's own field names and nesting**, in camelCase: `shopify.product.title`, `shopify.inventoryItem.measurement.weight`, `shopify.shippingAddress.city`.
* **Lists are plain arrays.** Where the API wraps a list in `edges { node }`, the app unwraps it.
* **Empty values are left out.** A field Shopify returns as `null` does not appear, and an object left with nothing in it disappears too. Empty lists stay (`"tags": []`).
* **Values are not converted.** Money stays a string (`"89.95"`), enums stay in Shopify's upper case (`ACTIVE`, `PAID`, `GRAMS`).

The app finds a record again by what it wrote there: an item by its `sku` (pyck's own field, not part of `data`), a customer, a picking order and an order line by `data.shopify.id`, and all items of a product by `data.shopify.product.id`.

Never write into `data.shopify` from pyck. The next sync replaces it, and a record whose `shopify.id` is gone is not found again and is created a second time.

## Auto and Custom mode

Under **Settings → Advanced**, every connection has a **Data mapping** with a **Mapping mode**:

| Mode       | What pyck receives                                                                                                                                                                       |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Auto**   | The app's own schemas `shopify_item_v1`, `shopify_customer_v1`, `shopify_order_v1`, `shopify_order_item_v1`, `shopify_file_v1` and `shopify_config_v1`, which the app creates and fills. |
| **Custom** | A schema you pick from the tenant for products, customers and orders, and optionally for order lines. Order lines without a schema of their own go into `shopify_order_item_v1`.         |

If the tenant needs no fields beyond what Shopify has, use **Auto** and stop reading here. Metafields can be added in Auto mode too (see [Values from metafields](#values-from-metafields)).

## Your schema must accept the shopify field

Every sync writes `data.shopify`, and pyck checks the whole `data` against your schema. Either:

* declare `"shopify": { "type": "object" }`, or
* leave `additionalProperties` open (it is by default).

Data mapping refuses a schema that declares `shopify` as anything other than an object, or that leaves it out while setting `additionalProperties: false`. The message reads, for example, `DataType "item_v2" does not allow the "shopify" key the app writes — add "shopify": { "type": "object" } to its properties`.

You can mark `shopify` as **required**: the app always writes it.

Declare as little as possible inside `shopify`. `{ "type": "object" }` is enough. If you do describe its contents, use Shopify's types (money as `string`, enums in upper case), keep `additionalProperties` open inside it, and do not mark anything inside it as required or restrict it with `enum`, `pattern`, `maxLength` or `format`. A value Shopify does not have is left out, and Shopify adds fields over time.

## What the shopify field contains

### Items (products)

One pyck item per Shopify **variant**. Variants without a SKU are never sent. The item's SKU is pyck's own `sku` field.

| Path                                                                              | Content                                                                       |
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `id`                                                                              | Variant ID (`gid://shopify/ProductVariant/…`)                                 |
| `title`, `displayName`                                                            | Variant title (`White`), and product plus variant (`Bloom Desk Lamp - White`) |
| `sku`, `barcode`                                                                  |                                                                               |
| `price`, `compareAtPrice`                                                         | Strings, e.g. `"89.95"`                                                       |
| `position`, `inventoryQuantity`                                                   | Integers                                                                      |
| `inventoryPolicy`                                                                 | `DENY` or `CONTINUE`                                                          |
| `taxable`                                                                         | Boolean                                                                       |
| `selectedOptions`                                                                 | List of `{ name, value }`                                                     |
| `inventoryItem.requiresShipping`, `.harmonizedSystemCode`, `.countryCodeOfOrigin` |                                                                               |
| `inventoryItem.measurement.weight`                                                | `{ value, unit }`, unit e.g. `GRAMS`, `KILOGRAMS`, `OUNCES`, `POUNDS`         |
| `createdAt`, `updatedAt`                                                          | ISO 8601 date-time                                                            |
| `product.id`                                                                      | Product ID (`gid://shopify/Product/…`)                                        |
| `product.title`, `.handle`, `.descriptionHtml`, `.vendor`, `.productType`         |                                                                               |
| `product.status`                                                                  | `ACTIVE`, `ARCHIVED`, `DRAFT` or `UNLISTED`                                   |
| `product.tags`                                                                    | List of strings                                                               |
| `product.publishedAt`, `.createdAt`, `.updatedAt`                                 |                                                                               |
| `metafields.<namespace>.<key>`                                                    | Auto mode only: metafields added to the schema                                |

The app reads up to 100 variants and 100 images per product.

### Customers

| Path                               | Content                                              |
| ---------------------------------- | ---------------------------------------------------- |
| `id`                               | Customer ID (`gid://shopify/Customer/…`)             |
| `firstName`, `lastName`            |                                                      |
| `defaultEmailAddress.emailAddress` |                                                      |
| `defaultPhoneNumber.phoneNumber`   |                                                      |
| `state`                            | e.g. `ENABLED`                                       |
| `verifiedEmail`, `taxExempt`       | Booleans                                             |
| `tags`                             | List of strings                                      |
| `note`                             |                                                      |
| `defaultAddress`                   | Address object, with its `id`                        |
| `addresses`                        | List of up to 10 address objects, each with its `id` |
| `createdAt`, `updatedAt`           |                                                      |
| `metafields.<namespace>.<key>`     | Auto mode only                                       |

An address object contains `firstName`, `lastName`, `company`, `address1`, `address2`, `city`, `province`, `provinceCode`, `country`, `countryCodeV2`, `zip` and `phone`.

### Picking orders (orders)

| Path                                                                                             | Content                                                       |
| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
| `id`                                                                                             | Order ID (`gid://shopify/Order/…`)                            |
| `name`, `number`                                                                                 | `#1001` and `1001`                                            |
| `email`, `phone`, `poNumber`, `note`                                                             |                                                               |
| `customer.id`                                                                                    | Customer ID; absent on guest orders                           |
| `displayFinancialStatus`, `displayFulfillmentStatus`                                             | e.g. `PAID`, `UNFULFILLED`                                    |
| `currencyCode`                                                                                   | e.g. `EUR`                                                    |
| `totalPriceSet`, `subtotalPriceSet`, `totalTaxSet`, `totalDiscountsSet`, `totalShippingPriceSet` | `{ shopMoney: { amount, currencyCode } }`, amount as a string |
| `taxesIncluded`, `test`                                                                          | Booleans; `test` marks a Shopify test order                   |
| `shippingAddress`, `billingAddress`                                                              | Address objects, without `id`                                 |
| `tags`                                                                                           | List of strings                                               |
| `statusPageUrl`                                                                                  |                                                               |
| `processedAt`, `cancelledAt`, `closedAt`, `createdAt`, `updatedAt`                               |                                                               |
| `metafields.<namespace>.<key>`                                                                   | Auto mode only                                                |

### Order lines

One pyck picking order item per Shopify line item that has a SKU and a variant, up to 100 per order. The quantity is pyck's own `quantity` field.

| Path                                             | Content                                   |
| ------------------------------------------------ | ----------------------------------------- |
| `id`                                             | Line item ID (`gid://shopify/LineItem/…`) |
| `name`, `title`, `variantTitle`, `sku`, `vendor` |                                           |
| `quantity`, `unfulfilledQuantity`                | Integers                                  |
| `taxable`, `requiresShipping`                    | Booleans                                  |
| `originalUnitPriceSet`, `totalDiscountSet`       | `{ shopMoney: { amount, currencyCode } }` |
| `variant.id`, `product.id`                       |                                           |

### Files (product images)

Product images are pyck files attached to the items, with `data` of the form `{ "shopify": { "id", "alt", "image": { "url", "width", "height" } } }`. The `id` is Shopify's `MediaImage` ID.

## Your own fields

In Custom mode, Data mapping lists every field of your schema except `shopify` as an **extra field**. Each one can take a source:

| Source                                                | Takes the value from                                                                          |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| **Shopify field**                                     | A field of the Shopify record, from the app's list (see [below](#shopify-fields-you-can-map)) |
| **Shopify metafield**                                 | A metafield with a definition in Shopify                                                      |
| **Product tags** / **Customer tags** / **Order tags** | The record's tags, as a list                                                                  |
| **Product image** / **Shopify file metafield**        | A file (file fields only)                                                                     |
| **Not synced**                                        | Nowhere                                                                                       |

A field without a source is **Not synced**: the app never writes it and never removes it. A mapping can be saved with fields left that way.

Order line schemas are the exception: a Shopify order line has no metafields or tags, so every extra field of an order line schema is **Not synced**.

### Do you need a field of your own?

Everything in [`data.shopify`](#what-the-shopify-field-contains) is already on the record. A workflow or a pyck screen that needs the vendor reads `data.shopify.product.vendor`. It does not need a `vendor` field.

Add a field of your own when:

* the value is **not in `data.shopify`**: a metafield, or one of the Shopify fields listed under [Shopify fields that are not in the shopify field](#shopify-fields-that-are-not-in-the-shopify-field);
* your workflow or UI needs a value **at a different place or in a different type**, e.g. a flat `weight_kg` as a number;
* the value is **warehouse data** Shopify does not have.

A mapped field holding a value that is also in `data.shopify` stores it twice. That is fine when you need it there, but both copies follow Shopify. Neither is a place to change the value.

### Types

A mapped value is converted to the type your schema declares for the field: `number` and `integer` become numbers, `boolean` becomes `true` or `false`, `string` becomes text. A field `price` of type `number` mapped to the Shopify field **Price** therefore receives `89.95`, while `data.shopify.price` keeps `"89.95"`. A value that cannot be converted (text into a number) is written unconverted, and pyck rejects the record if the type does not allow it.

Tags always arrive as a list. A field filled from tags must be an `array` of strings.

### The Auto-map button

The **Auto-map same names** button fills in a source for each extra field that has no source yet or is set to **Not synced**, by looking for a Shopify value of the same name in two places:

* the **Shopify fields** the app can read, named by their path in Shopify's Admin GraphQL API (`inventoryPolicy`, `product.seo.title`, `inventoryItem.harmonizedSystemCode`, `poNumber`, `numberOfOrders`, …);
* the store's **metafields**, by key without the namespace (`ean` ← `custom.ean`). Metafields in the app's own `app--…` namespaces are skipped.

The comparison:

* **camelCase and snake\_case are the same name.** A pyck field `harmonized_system_code` inside an object `inventory_item` matches Shopify's `inventoryItem.harmonizedSystemCode`, and so does `inventoryItem.harmonizedSystemCode`. Upper and lower case are ignored as well (`EAN` matches `custom.ean`). Nothing else is folded together: no synonyms, no abbreviations, no hyphens.
* **Paths are compared whole.** An object field must have the same name as Shopify's object: `inventory_item.country_code_of_origin` matches, `origin.country` does not.
* **One candidate or nothing.** If two sources fit (e.g. `custom.ean` and `erp.ean`, or a Shopify field and a metafield of the same name), the field is left for you to choose.
* **The type is not compared.** A text field can be matched to a numeric Shopify value. Check the result.
* **Only a suggestion.** The result is shown in the list and only takes effect when you save. Someone still has to open Data mapping on every store and press the button.

### Shopify fields you can map

| Entity    | Shopify fields                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Items     | `sku`, `barcode`, `price`, `compareAtPrice`, `taxable`, `inventoryPolicy`, `position`, `title`, `displayName`, `inventoryItem.measurement.weight.value`, `inventoryItem.measurement.weight.unit`, `inventoryItem.requiresShipping`, `inventoryItem.harmonizedSystemCode`, `inventoryItem.countryCodeOfOrigin`, `product.title`, `product.handle`, `product.vendor`, `product.productType`, `product.status`, `product.descriptionHtml`, `product.description`, `product.createdAt`, `product.updatedAt`, `product.publishedAt`, `product.onlineStoreUrl`, `product.totalInventory`, `product.hasOnlyDefaultVariant`, `product.isGiftCard`, `product.seo.title`, `product.seo.description` |
| Customers | `firstName`, `lastName`, `displayName`, `email`, `phone`, `note`, `locale`, `state`, `taxExempt`, `verifiedEmail`, `numberOfOrders`, `amountSpent.amount`, `amountSpent.currencyCode`, `createdAt`, `updatedAt`, `emailMarketingConsent.marketingState`, `smsMarketingConsent.marketingState`, and the `defaultAddress.…` fields                                                                                                                                                                                                                                                                                                                                                          |
| Orders    | `name`, `email`, `phone`, `note`, `poNumber`, `currencyCode`, `presentmentCurrencyCode`, `displayFinancialStatus`, `displayFulfillmentStatus`, `confirmed`, `closed`, `cancelledAt`, `cancelReason`, `processedAt`, `createdAt`, `updatedAt`, `test`, `taxesIncluded`, `sourceName`, `totalPriceSet.shopMoney.amount`, `subtotalPriceSet.shopMoney.amount`, `totalTaxSet.shopMoney.amount`, `totalDiscountsSet.shopMoney.amount`, `totalShippingPriceSet.shopMoney.amount`, `totalWeight`, and the `shippingAddress.…` and `billingAddress.…` fields                                                                                                                                      |

The address fields are `firstName`, `lastName` and `name` (order addresses only), `company`, `address1`, `address2`, `addressLines`, `city`, `province`, `provinceCode`, `country`, `countryCodeV2`, `zip` and `phone`. `addressLines` is address line 1 and 2 joined with a comma, for a pyck field that has room for only one street line.

Values Shopify calls something else, or that the app cannot read at all, need a source chosen by hand or have none: an item's `cost` has no Shopify field, a customer's `orders_count` is `numberOfOrders` in Shopify, and `total_spent` is `amountSpent.amount`.

### Shopify fields that are not in the shopify field

These can only reach pyck through a field of your own:

| Entity    | Fields                                                                                                                                                                                        |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Items     | `product.description` (plain text), `product.onlineStoreUrl`, `product.totalInventory`, `product.hasOnlyDefaultVariant`, `product.isGiftCard`, `product.seo.title`, `product.seo.description` |
| Customers | `displayName`, `email`, `phone`, `locale`, `numberOfOrders`, `amountSpent.amount`, `amountSpent.currencyCode`, `emailMarketingConsent.marketingState`, `smsMarketingConsent.marketingState`   |
| Orders    | `presentmentCurrencyCode`, `confirmed`, `closed`, `cancelReason`, `sourceName`, `totalWeight`                                                                                                 |

### Values from metafields

If a value lives in a Shopify metafield, name the pyck field **after the metafield key**, without the namespace. **Auto-map same names** then connects them in one click. The metafield needs a definition in Shopify (**Settings → Custom data**), and for items it must be a **product** metafield: variant metafields are not offered.

| Metafield in Shopify   | pyck field                        | Result                                                                        |
| ---------------------- | --------------------------------- | ----------------------------------------------------------------------------- |
| `custom.ean`           | `ean`                             | Mapped automatically                                                          |
| `custom.pallet_height` | `pallet_height` or `palletHeight` | Mapped automatically (camelCase and snake\_case are treated as the same name) |
| `custom.ean`           | `ean_code`                        | Not mapped; choose the source by hand                                         |

In **Auto** mode there are no fields of your own. Instead, the **Shopify metafields** block in Data mapping checks which metafields of the store the schemas do not carry yet, and **Add these to my schemas** declares them in the app's schemas. Their values then arrive at `data.shopify.metafields.<namespace>.<key>`, e.g. `data.shopify.metafields.custom.material`.

### Files

A field of type file (e.g. a shipping label stored in a file metafield) is named after the metafield key in the same way. File fields must be at the top level of the schema. Inside an object, they are not delivered. Product images can be mapped to any file field on the item schema.

Files from file metafields are attached to the record in both modes, whether a field is mapped to them or not. See [What a sync overwrites in pyck](/shopify-app/data-mapping.md#files).

### Warehouse-only data

Fields only the warehouse maintains (storage zone, pallet type, handling notes, a workflow status) need no Shopify counterpart. Give them names of your own and leave them without a source. The app then never touches them. See [What a sync overwrites in pyck](/shopify-app/data-mapping.md#fields-set-to-not-synced).

### Nested objects

An object field of your own (e.g. `delivery_address`) is listed in Data mapping one sub-field at a time, up to three levels deep, and each sub-field gets its own source. Auto-map compares the whole path, so `delivery_address.city` is not picked up, because Shopify calls that address `shippingAddress`.

The order's addresses are already in `data.shopify.shippingAddress` and `data.shopify.billingAddress`. Add an address object of your own only if your workflow needs the address in another place or shape.

## Types must match

pyck validates every record against its schema. If one value does not fit, pyck rejects the **whole record** and nothing on it is updated. Activity shows a failed entry such as `jsonschema validation failed: at '/weight_kg': got string, want number`.

Typical mistakes:

| Mistake                                                                                                | Correct                                                                         |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| `shopify` declared as `string`, or missing while `additionalProperties` is `false`                     | `"shopify": { "type": "object" }`                                               |
| Sub-fields of `shopify` declared with your own types, e.g. `price` as `number`                         | Leave `shopify` undeclared inside, or use Shopify's types (`price` is a string) |
| A field filled from tags declared as `string`                                                          | `array` of strings                                                              |
| A mapped field marked `required` that Shopify can leave empty                                          | Not required, or a source that always has a value                               |
| A short `maxLength` on a mapped field, e.g. a weight unit (`POUNDS`) into a three-character unit field | A source whose values fit, or no limit                                          |

Rules for the schema as a whole:

* **Leave `additionalProperties` open**, or at least declare `shopify`. The app's own schemas are open for the same reason.
* **Mark only `shopify` and your own warehouse fields as required**, and only fields that always have a value. A mapped field whose value is cleared in Shopify is removed, and a required field without a value makes pyck reject the record.
* **Do not restrict mapped fields** with `maxLength`, `minLength`, `pattern`, `format` or your own `enum` unless every Shopify value fits.

## Example

An item schema for a tenant that extends Shopify's data with an EAN from a metafield and two warehouse fields:

```json
{
  "type": "object",
  "required": ["shopify"],
  "properties": {
    "shopify":      { "type": "object", "description": "Written by the Shopify app" },

    "ean":          { "type": "string", "description": "From metafield custom.ean" },
    "storage_zone": { "type": "string", "description": "Maintained in pyck" },
    "pallet_type":  { "type": "string", "enum": ["EUR", "ONEWAY"], "description": "Maintained in pyck" }
  }
}
```

In Data mapping, this schema shows exactly three extra fields. **Auto-map same names** connects `ean` to `custom.ean`. `storage_zone` and `pallet_type` have no source and stay **Not synced**. Title, price, vendor and everything else Shopify knows are read from `data.shopify` (`shopify.product.title`, `shopify.price`, `shopify.product.vendor`) without any setup.

## Schemas from before the shopify field

Up to October 2026, the app wrote Shopify data as top-level snake\_case keys (`shopify_id`, `title`, `price`, `shipping_address`, …) and filled schema fields with those names on its own. It no longer does. In a schema designed for that:

* those fields are now extra fields like any other. Without a source they are **Not synced** and keep their last value forever;
* the schema has to accept `shopify` before Data mapping saves it.

To move a tenant over, remove the old keys from the schema (or give them a source if a workflow still reads them), make sure it accepts `shopify`, run **Check connection** on the connection's card under **Settings** so the app's own schemas are updated, and send everything again with **Sync now** on the Home page.

Items are found again by their SKU and updated; their old keys stay next to `shopify` until you remove them. Customers, picking orders and order lines synced before carry no `data.shopify.id`, so the new sync does not find them and creates them again. Remove them from pyck before you send everything, or they exist twice.

## Checklist

Before connecting a tenant to Shopify, check each schema for:

* [ ] `shopify` is declared as an object, or `additionalProperties` is not `false`.
* [ ] Nothing inside `shopify` is required or restricted.
* [ ] No field of your own copies a value that is already in `data.shopify`, unless a workflow needs it there.
* [ ] Mapped fields have types the Shopify value converts to; fields filled from tags are arrays.
* [ ] Only `shopify` and fields that always have a value are required.
* [ ] Fields filled from metafields are named after the metafield key.
* [ ] File fields are at the top level.
* [ ] Warehouse-only fields have their own names and no source.

## Related pages

* [What a sync overwrites in pyck](/shopify-app/data-mapping.md): who owns which field once the mapping is set.
* [When something looks wrong](/shopify-app/troubleshooting.md#a-record-fails-with-jsonschema-validation-failed): records rejected by pyck.
* [Setup](/shopify-app/setup.md): connecting a tenant.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.pyck.cloud/shopify-app/schema-design.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
