Automating JSONata Shopify mapping with an AI coding agent
How the Graftport CLI lets an AI agent investigate source rows, fix JSONata mapping errors against structured validation codes, and publish versioned mappings.
How the Graftport CLI lets an AI agent investigate source rows, fix JSONata mapping errors against structured validation codes, and publish versioned mappings.
A Shopify migration lives or dies on its mapping expressions. In Graftport those expressions are JSONata — a small, JSON-native transformation language — and the iteration loop around them is the single biggest time-sink of a typical replatform. This guide covers how the Graftport CLI exposes that loop in a shape a coding agent can drive: pulling samples, running the validator, reading structured error codes, and publishing new mapping versions.
If you have not yet seen the bigger picture, start with AI Shopify migration with a coding agent. This guide goes one level deeper into the mapping layer itself.
Every source platform — Magento, WooCommerce, source Shopify, and the others to come — exposes a different JSON shape. Shopify expects its own. Something has to translate between them, and that translation needs to be:
JSONata fits all three. The expression is plain text, it composes field-level extractions and transformations declaratively, and it runs deterministically against the JSON the source returned. Crucially for AI work, the input and output are both structured JSON, so the feedback loop is mechanical rather than narrative.
Graftport stores one JSONata expression per resource type per migration. Products, customers, orders, redirects, collections — each gets its own mapping, each evolves on its own version history.
The single most common failure mode of generic "AI migration" attempts is a model that hallucinates the source schema. The fix is to never let it. The Graftport CLI gives the agent direct access to live source rows:
graftport source rows <migration_id> product --limit 5
This streams five real product rows from the connected source store as JSON on stdout. No schema diagram, no documentation page — the actual shape the source API returns, including the custom attributes, the half-filled fields, and the historical quirks that no diagram captures.
For deeper inspection of a single record (the raw payload from the source before Graftport's intermediate shape is applied), the agent can use:
graftport source raw <migration_id> product <source_id>
The agent uses these two commands as its eyes. Every mapping draft starts with at least one sample call so the expression is grounded in real data, not a guess.
The mapping editing loop runs through mappings validate. The shape:
graftport mappings validate <mapping_id> \
--jsonata current.jsonata \
--limit 50 --pretty
The validator pulls a fresh sample of source rows, runs the JSONata
expression against each, and reports per-row failures with structured
error codes. The exit code is 0 only when every sampled row passes.
The bundled skill document teaches the agent which JSONata pattern solves each error code. Three common categories worth knowing about:
AMOUNT_MISMATCH — a numeric field's units do not match
Shopify's expectation. The classic case is weight: Magento stores
kilograms, Shopify wants grams. The fix is a unit conversion inside
the mapping expression.TYPE_COERCION — a value's type does not match the destination
field. A status string that should be a boolean, a numeric ID
arriving as a string, a date stored as a Unix timestamp instead of
ISO 8601. The fix is an explicit conversion.MISSING_REQUIRED — a Shopify-required field is missing or
null on a source row. The fix is either a fallback expression
(default value, derived value from another field) or a filter that
excludes rows that genuinely cannot map.Other structured codes exist; the skill documents what the validator returns in your installed version. Treat the codes as the contract: the agent should not paper over a failure by adding a try-catch, it should fix the underlying expression.
The patterns below are the kind of expression the agent ends up writing repeatedly. They are short on purpose — JSONata's value is that small expressions cover a lot of ground.
Weight kilograms to grams (typical AMOUNT_MISMATCH fix):
{
"weight": weight_in_kg * 1000,
"weight_unit": "GRAMS"
}
Magento status enum to Shopify status (typical TYPE_COERCION fix
when source carries an integer and destination expects a string):
{
"status": status = 1 ? "ACTIVE" : "DRAFT"
}
Comma-separated tag string to Shopify tag array (a common shape mismatch — Shopify expects a list, the source stored a single string):
{
"tags": $split(tags_csv, ",") ~> $map(function($t) { $trim($t) })
}
Default for a missing required field (typical MISSING_REQUIRED
fix, where the source sometimes omits the field entirely):
{
"vendor": vendor_name ? vendor_name : "Unbranded"
}
These are the building blocks. A real product mapping is a single JSONata expression that emits the full destination object with dozens of fields handled this way. The agent's iteration is mostly extending and fixing one such expression per resource.
The cycle the bundled skill teaches the agent is rhythmic:
graftport mappings show <mapping_id> --raw > current.jsonatagraftport source rows <migration_id> <resource> --limit 5current.jsonata to handle whatever the samples revealed.graftport mappings validate <mapping_id> --jsonata current.jsonata --limit 50graftport mappings publish <mapping_id> --jsonata current.jsonata --notes "<what changed>"The publish is metadata only — it bumps the mapping version and flags downstream records for re-load on the next run. No Shopify write happens here. Because publishing is cheap and reversible, the agent can ship freely. If a later run reveals the new version was wrong, restoring the previous version is one command (and the run reprocesses the affected records on the next pass).
The --notes flag is worth using. The notes show up in the mapping's
version history, which is the audit trail when you (or another agent
session) need to understand why v7 looks different from v6. A useful
convention: lead with the error code or symptom the version was
fixing.
A validate that passes at --limit 50 is a strong signal but not the
whole truth. The validator runs the JSONata expression in isolation;
a dry-run runs the full extract → transform → load pipeline against
real Shopify API contracts (without actually pushing). Some shape
failures only surface there.
graftport runs start <migration_id> --dry-run
A dry-run is agent-allowed and costs nothing on the load side. The output is browsable from the Data tab of the migration in the app — you see the exact payload Shopify would receive for every record, field by field. If a Shopify validation fails (an enum value Shopify does not accept, a price format mismatch), it shows up here with a distinct error from the validator's codes, and the agent loops back to the JSONata expression to fix it.
Only after the dry-run is green does the agent compose the real
graftport runs start <migration_id> command — which is human-gated,
and which the agent presents for your approval rather than executing.
That handoff is covered in Human approval gates for AI Shopify
migrations.
Every publish creates a new immutable mapping version. The numbers matter for two reasons.
First, the version flag on a record. When a load writes a record to Shopify, it stamps the mapping version it used. If you later discover v5 was wrong, the records loaded under v5 are identifiable and can be reloaded under the corrected v6 on the next run.
Second, the rollback path. Rolling back a mapping is just another publish — there is no destructive revert. The agent (or you) takes a known-good JSONata body and publishes it again as a new version. The simplest source for the known-good body is the local file you kept from when v5 last validated cleanly, or any copy-and-pasted snapshot. Once you have it:
graftport mappings publish <mapping_id> --jsonata rollback.jsonata \
--notes "rollback to v5 logic"
The mapping's version history now reads … v5 v6 v7 where v7 has
the same body as v5. Subsequent runs use v7; v6 stays in history but
no longer drives loads. Publish is agent-allowed, so the agent can
roll back on its own — but it cannot start the re-load that picks the
rollback up. That stays human-gated.
The same versioning means the agent never destroys work. Even a catastrophic mistake is a question of publishing a new version, not undoing a deletion.
A real migration has six or seven resources in active iteration — products, customers, orders, redirects, collections, pages, blogs. The cadence that works in practice:
Within each resource, the validate / publish loop is the same. The agent does not need to know your cadence; you set the order by telling it which mapping to work on next.
Two things in the mapping layer are still yours.
The mapping template is set up in the app. When you create the migration, the resource templates seed initial mapping expressions from the upstream library. The CLI works on the per-migration mapping copies, not the upstream templates.
Domain-specific business logic is your call. "Should this Magento status map to Shopify draft or active?" is a product decision. The agent will surface the choice and propose a reasonable default; you sign off on the semantics. The skill is explicit that the agent should ask rather than guess on questions like this.
The mechanical part — getting from a clean intent to a working JSONata expression, validated against real samples — is what the agent absorbs.
Want to point your own agent at a real mapping? Sign up at app.graftport.com and start a migration, then jump to /ai-migrations for the install commands.
Connect a source store, dry-run a migration, see the exact Shopify result before a single record lands. The same platform your team will use on go-live night.