shopify-admin-bulk-customer-tag-update
SkillCommerce & financeAdds and/or removes tags across a filtered set of customers — supports query-based selection, explicit ID lists, and union/replace tag modes.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the shopify-admin-bulk-customer-tag-update skill
What this skill tells your AI
The instructions your AI receives, as published by 40rty-ai/shopify-admin-skills in skills/customer-support/shopify-admin-bulk-customer-tag-update/SKILL.md and read by ahel’s review.
Purpose
Applies bulk tag changes (add, remove, or both) to customers selected by a query filter (e.g., total_spent:>=500, tag:newsletter) or by an explicit list of customer GIDs. Tags are how Shopify segments customers for discounts, marketing, and support workflows; this skill makes batch changes safe, dry-runnable, and auditable. Use when migrating from one tag taxonomy to another, when retiring a campaign-specific tag, or when applying a new segment tag identified by an analytics report.
Prerequisites
- Authenticated Shopify CLI session:
shopify store auth --store <domain> --scopes read_customers,write_customers - API scopes:
read_customers,write_customers
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| store | string | yes | — | Store domain (e.g., mystore.myshopify.com) |
| format | string | no | human | Output format: human or json |
| dry_run | bool | no | true | Preview matching customers and the planned tag changes without executing mutations |
| filter | string | conditional | — | Customer query filter (e.g., tag:newsletter, total_spent:>=500); required if customer_ids is omitted |
| customer_ids | array | conditional | — | Explicit list of customer GIDs; required if filter is omitted |
| add_tags | array | no | [] | Tags to add (union with existing tags) |
| remove_tags | array | no | [] | Tags to remove (set difference) |
| mode | string | no | merge | Tag write mode: merge (apply add/remove to existing) or replace (overwrite tags entirely with add_tags only) |
| max_customers | integer | no | 1000 | Run-size cap; abort if filter matches more than this |
Safety
⚠️ Step 2 executes one
customerUpdatemutation per customer in the matched set. Tag changes are immediate and visible to staff and to any apps reading customer tags (loyalty, marketing automation, segmentation).mode: replaceoverwrites existing tags entirely — manually-applied operational tags will be lost. The default isdry_run: trueandmode: merge. Always run dry-run first, review the matched count, and confirmadd_tags/remove_tagsare spelled correctly — Shopify tags are case-sensitive.
Workflow Steps
-
OPERATION:
customers— query Inputs: Whenfilteris set:query: <filter>,first: 250, pagination cursor. Whencustomer_idsis set: batch query withquery: "id:<id1> OR id:<id2> ..."(chunk into batches of 25 IDs). Selectid,displayName,defaultEmailAddress { emailAddress },tags. Expected output: Customer list with current tags. Abort ifmatch_count > max_customers. -
For each matched customer, compute the target tag set:
- If
mode: merge:target = (existing ∪ add_tags) \ remove_tags - If
mode: replace:target = add_tags(remove_tags ignored) Skip the customer iftarget == existing(no-op).
- If
-
OPERATION:
customerUpdate— mutation Inputs: For each customer with a non-empty diff:input: { id: <customer_id>, tags: <target_tag_array> }Expected output:customer.id,customer.tags,userErrors; collect failures
GraphQL Operations
# customers:query — validated against api_version 2025-01
query CustomersForBulkTagging($query: String!, $after: String) {
customers(first: 250, after: $after, query: $query) {
edges {
node {
id
displayName
firstName
lastName
defaultEmailAddress {
emailAddress
}
tags
numberOfOrders
amountSpent {
amount
currencyCode
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
# customerUpdate:mutation — validated against api_version 2025-01
mutation CustomerTagsUpdate($input: CustomerInput!) {
customerUpdate(input: $input) {
customer {
id
displayName
tags
}
userErrors {
field
message
}
}
}
Session Tracking
Claude MUST emit the following output at each stage. This is mandatory.
On start, emit:
╔══════════════════════════════════════════════╗
║ SKILL: Bulk Customer Tag Update ║
║ Store: <store domain> ║
║ Started: <YYYY-MM-DD HH:MM UTC> ║
╚══════════════════════════════════════════════╝
After each step, emit:
[N/TOTAL] <QUERY|MUTATION> <OperationName>
→ Params: <brief summary of key inputs>
→ Result: <count or outcome>
If dry_run: true, prefix every mutation step with [DRY RUN] and do not execute it.
On completion, emit:
For format: human (default):
══════════════════════════════════════════════
BULK TAG UPDATE OUTCOME
Filter: <filter or "<n> explicit IDs">
Mode: <merge|replace>
Add tags: <list>
Remove tags: <list>
Customers matched: <n>
Customers updated: <n> (or "skipped — dry_run")
No-op (already in state): <n>
Errors: <n>
Output: bulk_tag_update_<date>.csv
══════════════════════════════════════════════
For format: json, emit:
{
"skill": "bulk-customer-tag-update",
"store": "<domain>",
"started_at": "<ISO8601>",
"completed_at": "<ISO8601>",
"dry_run": true,
"mode": "merge",
"outcome": {
"matched": 0,
"updated": 0,
"noop": 0,
"errors": 0,
"add_tags": [],
"remove_tags": [],
"output_file": "bulk_tag_update_<date>.csv"
}
}
Output Format
CSV file bulk_tag_update_<YYYY-MM-DD>.csv with columns:
customer_id, name, email, previous_tags, tags_added, tags_removed, new_tags, status
The status column reports updated, noop, or error: <message>.
Error Handling
| Error | Cause | Recovery |
|---|---|---|
THROTTLED | API rate limit exceeded | Wait 2 seconds, retry up to 3 times |
userErrors on customerUpdate | Invalid input or read-only customer | Log error, skip customer, continue |
match_count > max_customers | Filter is too broad | Refine filter or raise max_customers deliberately |
Both filter and customer_ids empty | No selection | Abort with parameter error |
| Tag is empty string | Whitespace-only entry | Strip and skip empty values |
| Case-mismatched remove_tag | Tags are case-sensitive | Re-run with exact casing |
Best Practices
- Always run with
dry_run: truefirst — review the matched count and a sample ofprevious_tags→new_tagsdiffs before committing. - Prefer
mode: merge(the default) for almost all use cases —mode: replaceis appropriate only when fully resetting a customer's tag taxonomy and you have an audited backup of prior tags. - Tags are case-sensitive:
VIPandvipare distinct in Shopify. Standardize casing in your taxonomy. - For ongoing operational segments, prefer date-stamped tag names (e.g.,
cohort-2026-Q2) so historical cohorts remain identifiable as new tags accumulate. - Pair with
vip-customer-identifierorcustomer-spend-tier-taggerto feed segment tags from analytics outputs into the customer record. - Use
remove_tagsas the cleanup pass after a campaign — leaving stale campaign tags clutters segmentation in marketing tools. - When
customer_idsis supplied directly (e.g., from another skill's CSV), the run is fully deterministic — no filter ambiguity.
Signals
- GitHub stars
- 187
- Forks
- 18
- Last commit
- Aug 2026
ahel review
S4info
community integration — published by 40rty-ai, not shopify
Automated review, not a security audit. Ruleset v1.
Advanced
- Catalog kind
- skill
- Gateway key
shopify-admin-bulk-customer-tag-update- Source
- github.com/40rty-ai/shopify-admin-skills