How to Decompose Product Sync with an Upsert-Product Subworkflow
Build a reusable child workflow in Spojit that creates or updates a single product in a target store, then call it from any parent catalog sync flow for every item.
What This Integration Does
When you sync a product catalog between stores, the same "does this product already exist? if so update it, otherwise create it" decision shows up in every flow you build. Copying that logic into a Shopify-to-BigCommerce sync, then again into a daily refresh, then again into a one-off migration, means three places to fix when the matching rule changes. This tutorial extracts that create-or-update (upsert) decision into one child workflow. A parent catalog sync loops over its product list and calls the child once per item through a Subworkflow node, passing the product payload as input. The child does the lookup, the branch, and the write, then returns a small result the parent can log.
The child is triggered by a Manual trigger, so its output is exactly the request body the parent hands it. Each call runs the child through its own trigger and nodes; its final output returns to the parent, and every call appears as its own execution-history entry, which makes per-product failures easy to find. Because changes to the child take effect immediately for all parents, you fix the matching rule once and every sync flow picks it up. Re-runs are idempotent by design: the child looks the product up first, so calling it twice with the same item updates rather than duplicates.
Prerequisites
- A BigCommerce connection (the target store in this example), added via Connections -> Add connection, with permission to read and write products.
- A Shopify connection (the source store in the parent flow), with read access to products.
- The built-in JSON utility connector (no auth) for shaping the upsert input and reading the result.
- A field you can match on across stores. This example matches on
sku; decide yours before you build (SKU, handle, or a custom field). - Two workflows: a child named
Upsert Productand a parent catalog sync. Build the child first.
Step 1: Start the child workflow with a Manual trigger
In a new workflow named Upsert Product, add a Trigger node and set its type to Manual. The Manual trigger's output is whatever request body the parent passes in, so define the shape you expect the parent to send. For this child, expect a single product object such as:
{
"sku": "TEE-BLK-M",
"name": "Black Cotton Tee",
"price": 24.95,
"description": "100% cotton crew neck",
"is_visible": true
}
Downstream nodes read these as {{ input.sku }}, {{ input.name }}, and so on. Keep the input flat and store-neutral so the same child can serve more than one parent later.
Step 2: Look up the product in the target store
Add a Connector node in Direct mode, choose the BigCommerce connector, and select the list-products tool. Filter by the match field so you get back only the matching product. Map the filter input from the trigger, for example set the SKU filter to {{ input.sku }}. Name the node so its output is easy to reference, for example lookup. Direct mode is the right choice here because this is a single, predictable read with no judgment required, so it costs no AI credits.
The tool returns a list. An empty list means the product does not exist yet; a non-empty list means it does and carries the existing product id you will need to update it.
Step 3: Branch on whether the product exists
Add a Condition node after the lookup. Use the JSON connector's length or get tool first if you want a clean count, or reference the lookup result directly in the condition. Set the condition to test whether a matching product was found, for example check that the first result's id is present:
{{ lookup.data.0.id }} is not empty
The true branch means "found, update it." The false branch means "not found, create it." Label both edges in the properties panel so the canvas stays readable.
Step 4: Update on the true branch
On the true branch, add a Connector node in Direct mode using the BigCommerce connector and the update-product tool. Pass the existing product id from the lookup as the target, and map the changed fields from the trigger input:
productId: {{ lookup.data.0.id }}
name: {{ input.name }}
price: {{ input.price }}
description: {{ input.description }}
Name this node updated. Only send the fields you actually sync; leaving a field out leaves the store's value untouched, which is usually what you want for an update.
Step 5: Create on the false branch
On the false branch, add a Connector node in Direct mode using the BigCommerce connector and the create-product tool. Map the full product from the trigger input, since nothing exists yet:
name: {{ input.name }}
sku: {{ input.sku }}
price: {{ input.price }}
description: {{ input.description }}
is_visible: {{ input.is_visible }}
Name this node created. Both branches now converge on a final result step.
Step 6: Return a clear result to the parent
The child's final output returns to the parent, so make it small and explicit. Add a Transform node (or a JSON connector set step) that builds a single result object the parent can log, for example:
{
"sku": "{{ input.sku }}",
"action": "updated",
"productId": "{{ updated.data.id }}"
}
Use the matching value from whichever branch ran. Keep the keys identical across both branches (sku, action, productId) so the parent never has to guess the shape. This object is what the parent receives as the Subworkflow node's output.
Step 7: Call the child from the parent catalog sync
Open or create your parent workflow. Start it with whatever trigger suits the sync (a Schedule trigger for a nightly refresh, or a Manual trigger for an on-demand run). Add a Connector node in Direct mode on the Shopify connector using list-products to pull the source catalog. Add a Loop node in ForEach mode over the product list. Inside the loop body, add a Subworkflow node, set Workflow to Upsert Product, and set Input to the per-item payload shaped for the child:
{
"sku": "{{ item.sku }}",
"name": "{{ item.title }}",
"price": "{{ item.variants.0.price }}",
"description": "{{ item.body_html }}",
"is_visible": true
}
The parent pauses on each iteration while the child runs, then continues with the child's returned result. Collect those results to build a per-product run summary if you want one.
Tips
- Keep the child's input contract store-neutral. If you later add a WooCommerce parent, you map its fields into the same
sku/name/pricekeys and the child needs no changes. - Use Miraxa, the intelligent layer across your automation, to scaffold the child fast. Try a prompt like "Add a BigCommerce
list-productsDirect mode node, then a Condition node that checks if{{ lookup.data.0.id }}is not empty, with update on true and create on false." Then fine-tune fields in the properties panel. - For large catalogs, watch source-store pagination:
list-productsreturns a page at a time, so page through the source in the parent rather than assuming one call returns everything. - Children appear as their own execution-history entries, so when one product fails you can open just that call instead of scanning the whole parent run.
Common Pitfalls
- Deep nesting. The child may itself call helpers, but avoid stacking many Subworkflow levels; keep the upsert child a single, shallow layer for clarity and easier debugging.
- Mismatched result keys between branches. If the update branch returns
productIdbut the create branch returnsid, the parent's logging breaks. Normalize both to the same keys in Step 6. - Matching on a non-unique field. If two products share the SKU you match on, the lookup returns more than one and the update may hit the wrong record. Confirm the match field is unique in the target store first.
- Forgetting the write scope. A connection with read-only product access lets
list-productssucceed but fails oncreate-productorupdate-product. Confirm write permission on the BigCommerce connection before going live.
Testing
Test the child in isolation first. Open Upsert Product, press Run, and pass a Manual trigger body for a product you know does not exist; confirm the false branch creates it and the result reads "action": "created". Run the same body again and confirm the true branch now updates it instead of creating a duplicate. Then test the parent against a tiny scope: temporarily point the source list-products at a handful of items (or slice the loop input), run once, and check that each child call appears as its own execution-history entry with the expected result. Only widen to the full catalog once both branches and the loop behave on the small set.