← All guides11 min read

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.

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.

1. Why JSONata

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:

  • Inspectable — a human (or an agent) can read the expression and see what it does, line by line.
  • Versionable — each iteration is an immutable history entry, not a destructive edit.
  • Pure — the same source row in produces the same destination row out, every time, with no side effects.

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.

2. The agent's first move: real samples

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.

3. The validator and its error codes

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.

4. JSONata patterns that show up over and over

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.

5. The validate / publish cycle

The cycle the bundled skill teaches the agent is rhythmic:

  1. Pull current. graftport mappings show <mapping_id> --raw > current.jsonata
  2. Pull samples. graftport source rows <migration_id> <resource> --limit 5
  3. Edit current.jsonata to handle whatever the samples revealed.
  4. Validate. graftport mappings validate <mapping_id> --jsonata current.jsonata --limit 50
  5. If errors, return to step 3. If clean, continue.
  6. Publish. graftport 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.

6. From a clean validate to a real dry-run

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.

7. Version semantics

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.

8. Per-resource cadence

A real migration has six or seven resources in active iteration — products, customers, orders, redirects, collections, pages, blogs. The cadence that works in practice:

  • Products first. The biggest and most complex shape. Settle the product mapping before touching anything else, because variants and inventory keying ripple downstream.
  • Customers and orders in parallel. Customer mappings tend to be short. Order mappings are large but mostly mechanical once line items resolve correctly to your product mapping output.
  • Redirects, pages, blogs last. Small mappings, low ceremony. Useful as warm-up tasks if you want to get a feel for the agent's output style before turning it loose on products.

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.

9. What the agent does not do

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.

Related reading

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.

Ready to migrate?

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.

Get started See the calculator
Related guides
Using the Graftport CLI with Claude Code: install to first publish
Set up the Graftport CLI and migration-engineer skill in Claude Code, then drive a real Shopify migration through the validate and publish l
Human approval gates for AI Shopify migrations: the Graftport contract
Why an AI coding agent should never push to Shopify on its own, and how Graftport's two-tier CLI contract enforces a human gate on every cos
AI Shopify migration with a coding agent: the Graftport playbook
How to run an AI Shopify migration end to end with Claude Code, Cursor, Windsurf, or Copilot driving the Graftport CLI, with the costly step