plugins/aem/project-management/skills/ops/SKILL.md
Execute AEM Edge Delivery Services admin operations - list admins, add/remove users, preview, publish, unpublish content, clear cache, sync code, reindex, generate sitemap, manage snapshots, view logs, manage jobs, list sites, configure org/site settings, manage secrets and API keys. Also supports Document Authoring (DA) operations via admin.da.live - list/get/put content, copy, move, delete, versioning, and DA-specific preview/publish. Use for any Edge Delivery Services administrative task.
npx skillsauth add adobe/skills opsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Execute admin operations on AEM Edge Delivery Services projects using natural language commands.
| Category | Examples | |----------|----------| | Content | preview /path, publish /path, unpublish /path, status /path | | Cache | clear cache /path, force clear cache | | Code | sync code, deploy code | | Index | reindex /path, remove from index | | Sitemap | generate sitemap | | Snapshots | create snapshot X, publish snapshot X, approve snapshot X | | Logs | show logs, show logs last hour | | Users | add user@email as author/publish/develop, remove admin user@email, who am i | | Jobs | list jobs, job status X, stop job X | | Sites | list sites, switch to site-X, use branch feature-X | | Config | show org config, show site config, update robots.txt | | Secrets | list secrets, create secret, delete secret | | API Keys | list API keys, create API key, revoke API key | | Tokens | list tokens, create token, revoke token | | Profiles | show profile config, create profile, delete profile | | Index Config | show index config, update index config (query.yaml) | | Sitemap Config | show sitemap config, update sitemap config (sitemap.yaml) | | Versioning | list versions, restore version, rollback config | | Pages | list pages, list all pages, show indexed pages | | DA (Document Authoring) | da list, da source /path, da copy, da move, da delete, da config, da update config, da versions, da create version, da upload media, da auth |
If user invokes the skill without a specific command (e.g., just /ops or "help me with ops"), show:
Edge Delivery Services Operations
Quick commands to try:
list pages - Show all indexed pages
who am i - Check your user profile
list sites - Show available sites
show site config - View site configuration
preview /path - Preview a content path
show logs - View recent activity
For the full command list: type help, /ops help, or what can you do? (slash commands may be /ops help or /ops what can you do? depending on your client).
Shell commands in this skill use POSIX-compatible syntax (works on macOS/Linux). On Windows:
The agent executing these commands should adapt syntax to the user's environment.
Analyze user request and load the appropriate resource module.
Before ANY operation, check ~/.aem/ops-config.json for a previously stored org:
ORG=$(node -e "
const fs = require('fs');
try {
const c = JSON.parse(fs.readFileSync(process.env.HOME + '/.aem/ops-config.json', 'utf8'));
process.stdout.write(c.org || '');
} catch(e) {}
")
echo "org=${ORG:-NOT SET}"
If ORG is set, confirm with the user:
"Previously used org:
{ORG}. Do you want to continue with this org, or use a different one?"
If ORG is empty, ask the user:
"What is your Config Service organization name? This is the
{org}part of your Edge Delivery Services URLs (e.g.,https://main--site--{org}.aem.page).Note: The org name may differ from your GitHub organization, especially in repoless multi-site setups."
Save org to ~/.aem/ops-config.json:
mkdir -p "${HOME}/.aem"
node -e "
const fs = require('fs');
const p = process.env.HOME + '/.aem/ops-config.json';
let c = {};
try { c = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e) {}
c.org = '{ORG_NAME}';
fs.writeFileSync(p, JSON.stringify(c, null, 2));
"
STRICTLY FORBIDDEN - Do NOT attempt any of these to get org name:
git remote -v - GitHub org often differs from Config Service orgfstab.yaml - Does not contain org nameONLY use the org name from:
~/.aem/ops-config.json)Do NOT proceed until org is confirmed.
Before ANY API call, check if IMS token exists:
IMS_TOKEN=$(node -e "
const fs = require('fs');
try {
const t = JSON.parse(fs.readFileSync(process.env.HOME + '/.aem/ims-token.json', 'utf8'));
if (t.imsToken && t.imsTokenExpiry > Math.floor(Date.now()/1000) + 60) {
process.stdout.write(t.imsToken);
}
} catch (e) {}
")
echo "auth=${IMS_TOKEN:+set}"
If IMS_TOKEN is empty, invoke the auth skill BEFORE proceeding:
Skill({ skill: "project-management:auth" })
IMPORTANT: Do NOT skip this step. Do NOT attempt any API calls without a valid token. Use Authorization: Bearer ${IMS_TOKEN} header for all API calls.
After auth is confirmed, load full config from ~/.aem/ops-config.json:
eval $(node -e "
const fs = require('fs');
try {
const c = JSON.parse(fs.readFileSync(process.env.HOME + '/.aem/ops-config.json', 'utf8'));
console.log('ORG=' + JSON.stringify(c.org || ''));
console.log('SITE=' + JSON.stringify(c.site || ''));
console.log('REF=' + JSON.stringify(c.ref || 'main'));
} catch(e) {
console.log('ORG='); console.log('SITE='); console.log('REF=main');
}
")
IMS_TOKEN=$(node -e "
const fs = require('fs');
try {
const t = JSON.parse(fs.readFileSync(process.env.HOME + '/.aem/ims-token.json', 'utf8'));
process.stdout.write(t.imsToken || '');
} catch (e) {}
")
echo "Config: org=$ORG site=$SITE ref=$REF auth=${IMS_TOKEN:+set}"
Fetch profile to verify auth and record user identity:
PROFILE_RESPONSE=$(curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer ${IMS_TOKEN}" \
"https://admin.hlx.page/profile")
HTTP_CODE=$(echo "$PROFILE_RESPONSE" | tail -n1)
PROFILE=$(echo "$PROFILE_RESPONSE" | sed '$d')
if [ "$HTTP_CODE" = "401" ]; then
echo "Auth token expired. Clearing cached token..."
rm -f "${HOME}/.aem/ims-token.json"
echo "REAUTH_REQUIRED"
exit 1
elif [ "$HTTP_CODE" != "200" ]; then
echo "Failed to fetch profile (HTTP $HTTP_CODE). Check network/API status."
exit 1
fi
# Profile response: {"profile": {"email": "...", "name": "...", "ttl": ...}}
eval $(echo "$PROFILE" | node -e "
const d = require('fs').readFileSync(0,'utf8');
try {
const p = JSON.parse(d).profile || {};
console.log('USER_EMAIL=' + JSON.stringify(p.email || ''));
console.log('USER_NAME=' + JSON.stringify(p.name || ''));
} catch(e) { console.log('USER_EMAIL=\"\"'); console.log('USER_NAME=\"\"'); }
")
echo "Authenticated as: $USER_EMAIL ($USER_NAME)"
Important: The /profile endpoint does not return a role. To determine if the user is admin or author on a site, check the site access config:
# Determine user role on the current site
ACCESS_RESPONSE=$(curl -s \
-H "Authorization: Bearer ${IMS_TOKEN}" \
"https://admin.hlx.page/config/${ORG}/sites/${SITE}.json")
# Check which role(s) the user's email appears in within access.admin.role
# Roles: admin, author, publish, basic_author, basic_publish, develop, config, config_admin
If an operation returns 403, inform the user which role is required. Key role requirements:
basic_author, author, publish, or adminbasic_publish, publish, or adminpublish or admindevelop or adminconfig, config_admin, or adminconfig_admin or adminauthor, publish, or adminDo not block all operations because role cannot be pre-determined — let the API enforce permissions and surface 403 errors.
Save email to ~/.aem/ops-config.json for future use.
Read resources/config.md for setup instructions if site or other values are missing.
| User Intent | Resource Module |
|-------------|-----------------|
| preview, publish, unpublish, status, delete preview | resources/content.md |
| cache, purge, clear cache, invalidate | resources/cache.md |
| sync code, deploy code, update code | resources/code.md |
| reindex, index, remove from index, search | resources/index.md |
| sitemap, generate sitemap | resources/sitemap.md |
| snapshot, staged release, bundle | resources/snapshots.md |
| logs, audit, activity | resources/logs.md |
| user, access, permission, who am i, add user, remove user | resources/users.md |
| job, bulk operation, stop job | resources/jobs.md |
| site, branch, switch, list sites | resources/sites.md |
| org config, site config, robots.txt | resources/config-api.md |
| secret, secrets, create secret, delete secret | resources/secrets.md |
| API key, apikey, create key, revoke key | resources/apikeys.md |
| token, tokens, access token | resources/tokens.md |
| profile config, profile settings | resources/profiles.md |
| index config, helix-index, search config | resources/index-config.md |
| sitemap config, helix-sitemap, sitemap rules | resources/sitemap-config.md |
| version, versions, history, rollback, restore | resources/versioning.md |
| pages, list pages, indexed pages, all pages | resources/pages.md |
| da, da list, da source, da copy, da move, da delete, da config, da versions | resources/da.md |
resources/code.md)| HTTP Response | What It Means | Required Action |
|---------------|---------------|-----------------|
| 200/201 | Success | Display result with full URLs (https://{ref}--{site}--{org}.aem.page{path}) |
| 202 | Async job started | Report job name and instruct: check job status {jobName} to track progress |
| 204 | Success (no body) | Confirm completion: "{action} completed for {path}" |
| 4xx/5xx | Error | Show API error verbatim, then suggest fix per Error Handling table |
Before reporting success:
/path, "homepage", "the nav", "footer"destination field (not JSON). Config uses form-data with config field.help, what can you do?, /ops help, /ops what can you do?, "list commands", "show available commands" — show the Help Response block in this file (no resource module).CRITICAL: Always confirm before executing destructive operations.
| Operation | Resource | Risk Level |
|-----------|----------|------------|
| Unpublish (single/bulk) | content.md | HIGH - Removes from live site |
| Bulk publish (> 50 paths) | content.md | MEDIUM - Large surface area, partial failures possible |
| Delete preview | content.md | MEDIUM |
| Delete code | code.md | HIGH - Affects all sites in repoless |
| Purge all cache (wildcard) | cache.md | MEDIUM - Site-wide cache miss spike |
| Remove from index | index.md | MEDIUM - Removes from search |
| Publish entire snapshot | snapshots.md | HIGH - Mass publish to live |
| Approve snapshot | snapshots.md | HIGH - Publishes all + clears snapshot |
| Delete snapshot | snapshots.md | MEDIUM |
| Remove resource from snapshot | snapshots.md | LOW - Changes staged release contents |
| Remove user | users.md | HIGH - Revokes access |
| Stop job | jobs.md | MEDIUM - Can leave content half-published |
| Delete org/site config | config-api.md | CRITICAL - Can break site |
| Config update (org/site) | config-api.md | HIGH - POST replaces entire config; can break site if malformed |
| Delete secret | secrets.md | HIGH - Can break integrations |
| Revoke API key | apikeys.md | HIGH - Can break CI/CD |
| DA delete | da.md | HIGH - Permanently deletes from DA |
| DA copy (overwrite) | da.md | MEDIUM - Can silently overwrite destination |
| DA move/rename (overwrite) | da.md | MEDIUM - Can silently overwrite destination |
| DA update config | da.md | HIGH - Can lock out all users if CONFIG write permission missing |
| Revoke token | tokens.md | HIGH - Can break access |
| Delete config version | versioning.md | MEDIUM - Permanently removes config history |
| Restore config version | versioning.md | HIGH - Replaces current config |
| Delete profile config | profiles.md | MEDIUM - Removes profile settings |
| Delete index config | index-config.md | MEDIUM - Removes search indexing rules |
| Delete sitemap config | sitemap-config.md | MEDIUM - Removes sitemap rules |
These operations don't delete anything but can still cause outages if misused:
| Operation | Risk | Guardrail |
|-----------|------|-----------|
| Config update (POST) | Malformed config can break the entire site | Always GET current config first, show to user, confirm change before POST |
| Code sync in repoless | Affects ALL sites sharing the repo | Always check site count first; warn and list all sites if > 1 |
| Bulk preview/publish (> 50 paths) | Large jobs tie up resources and can fail partially | Show path list, confirm, suggest batching for > 100 paths |
| Wildcard bulk operations | Can trigger thousands of jobs | Explain that /* creates an async job that may process all pages |
| DA config update | Must include CONFIG write permission or locks out everyone | Validate config JSON includes a CONFIG write entry before sending |
Before ANY destructive operation:
~/.aem/ims-token.json (not in project-config).claude-plugin/ directory MUST be in .gitignore (contains org/site context)Secrets, API keys, and access tokens return their value only once at creation.
Before creating:
After creation:
.claude-plugin/project-config.json or any tracked file.Before ANY config update (org, site, profile, or DA config):
/*), explain that this creates an async job and may process thousands of pages.| Scenario | Recovery |
|----------|----------|
| Accidental unpublish | Re-publish: POST /live/{org}/{site}/{ref}/{path} |
| Accidental config delete | Restore from version: POST /config/{org}.json?restoreVersion={id} (list versions first) |
| Accidental secret/key delete | Cannot be recovered — create a new one and update all integrations |
| Bad config update broke site | List config versions, find last good one, restore it |
| Bulk job running wild | Stop it: DELETE /job/{org}/{site}/{ref}/{topic}/{jobName} |
| DA content accidentally deleted | Check if a version exists via /versionlist/ — restore from version if available |
| DA config locked everyone out | Requires Cloudflare KV access to fix — escalate to DA admin team |
Many of these endpoints return secrets, credentials, PII, or organizational metadata. Operating on a real customer org is normal usage — but the responses must be handled with care so they do not leak into chat history, terminal scrollback, or memory.
| Endpoint | Sensitive content |
|----------|-------------------|
| /config/{org}/apiKeys.json, /config/{org}/sites/{site}/apiKeys.json | API key IDs, expiration, role/subject metadata; value (JWT) on create |
| /config/{org}/sites/{site}/tokens.json | Token IDs; value (hlx_…) on create |
| /config/{org}/sites/{site}/secrets.json | Secret names; value on create |
| /config/{org}.json, /config/{org}/sites/{site}.json | User emails, role mappings, allowed domains, content source URLs, contentBusId |
| /config/{org}/users.json | User emails and IDs |
| /log/{org}/{site}/{ref} | User emails, IPs, paths edited, timestamps |
| /profile | IMS user ID, session IDs, scopes |
These rules apply unconditionally on every org — personal, sandbox, dev, stage, prod, customer. There is no carve-out: customer dev/stage/sandbox environments hold real PII just like prod. Do not rationalize past them — the user's verb ("list", "show", "get") is a request for the data, NOT a request to un-redact. The redacted view IS the requested data.
<3 admin users> or j***@example.com on the first response, regardless of org type or naming.value field a second time. It must be displayed exactly once at creation, with the instruction to copy it now. Do not include it in any later message, summary, or memory write.Before the first sensitive operation against any org in a session, surface a one-line note:
"Querying org
{org}. I'll redact emails by default — say 'show full' to see un-redacted output."
This applies uniformly. The org's name (test/dev/stage/prod/personal) does not change the rule.
Never write to memory:
value fieldshlx_… secret strings/profileIf the user asks you to remember something derived from a sensitive endpoint, save the abstract fact (e.g., "this org has CI/CD via API keys") not the identifiers.
Some endpoints create a credential on any POST, even with an empty body — defaults are filled in by the server, and the secret value is returned exactly once. This applies to:
/config/{org}/apiKeys.json (org API keys)/config/{org}/sites/{site}/apiKeys.json (site API keys)/config/{org}/sites/{site}/tokens.json (site tokens)/config/{org}/sites/{site}/secrets.json (site secrets)Never POST to these endpoints to "probe", "test", or "check" the API. Only POST when the user has explicitly asked to create a credential, with the intended role/scope/expiration provided. See apikeys.md and tokens.md for the full create flow.
If user provides an AEM URL instead of separate org/site/path values, extract context:
# Pattern: https://{ref}--{site}--{org}.aem.page{path} or .aem.live{path}
# The hostname is split on -- (double hyphen), not on single - (hyphenated org/site names use single -).
URL="$USER_INPUT"
if echo "$URL" | grep -q '\.aem\.page\|\.aem\.live'; then
DOMAIN=$(echo "$URL" | cut -d'/' -f3)
HOST_PART=$(echo "$DOMAIN" | cut -d'.' -f1)
REF=$(echo "$HOST_PART" | awk -F'--' '{print $1}')
ORG=$(echo "$HOST_PART" | awk -F'--' '{print $NF}')
SITE=$(echo "$HOST_PART" | awk -F'--' '{
r=""; for(i=2;i<NF;i++) r=(r==""?"":r"--")$i; print r
}')
URL_PATH=$(echo "$URL" | sed 's|https://[^/]*||')
URL_PATH=${URL_PATH:-/}
echo "Parsed from URL: org=$ORG site=$SITE ref=$REF path=$URL_PATH"
fi
Examples (hyphenated ref / site / org): uat--hmns-uat-kw--alshaya-axp.aem.page → ref=uat, site=hmns-uat-kw, org=alshaya-axp.
Use this when user pastes a URL like https://main--mysite--myorg.aem.page/en/products to extract the path for operations. Still follow the standard org/auth flow for config validation.
This skill works with AEM Edge Delivery Services projects that are:
access.admin.role). The Admin API defines eight roles: admin, author, publish, develop, basic_author, basic_publish, config, and config_admin. Each operation needs the matching permission; if the user lacks it, the API returns 403. Do not assume only "admin" or "author" — use the 403 guidance and role table earlier in this file when explaining permission errors.When API returns an error, explain the cause and how to fix it:
| HTTP Code | Cause | Tell User | Fix |
|-----------|-------|-----------|-----|
| 400 | Malformed request | "The request format is invalid. Check the path or payload syntax." | Review path format, ensure JSON/YAML is valid |
| 401 | Token expired or missing | "Your session has expired. You need to log in again." | Run Skill({ skill: "project-management:auth" }) |
| 403 | Insufficient permissions | Show actual API error message. If none, say: "You don't have permission for this operation." | Contact site admin to grant access |
| 404 (on path) | Content doesn't exist | "The path '{path}' was not found. Check if it exists in your content source." | Verify path spelling, check SharePoint/Drive |
| 404 (on org/site) | Org or site not configured | "The organization '{org}' or site '{site}' is not found. It may not be onboarded to Admin Service." | Verify org/site names, contact Adobe support if new project |
| 409 | Conflict (e.g., already exists) | "This resource already exists. Use update instead of create." | Use POST instead of PUT |
| 422 | Invalid content | "The content failed validation: {error details from API}" | Fix the specific validation error returned |
| 429 | Rate limited | "Too many requests. Wait a moment before retrying." | Wait 30-60 seconds, then retry |
| 500 | Server error | "The Admin Service encountered an error. This is temporary." | Wait and retry; if persistent, check status.adobe.com |
| 502/503 | Service unavailable | "The Admin Service is temporarily unavailable." | Wait a few minutes and retry |
Always show the actual API error message when available - it often contains specific details.
When the user wants the command list, show the block below. Match help, what can you do?, the same with a /ops prefix (e.g. /ops help, /ops what can you do?), or short phrases like list commands / show available commands:
Content Operations:
preview /path - Update preview
publish /path - Publish to live
unpublish /path - Remove from live (requires publish or admin role)
status /path - Check preview/live status
Cache Operations:
clear cache /path - Purge CDN cache
force clear cache - Force purge
Code Operations:
sync code - Deploy latest code
Index Operations:
reindex /path - Re-index for search
Sitemap:
generate sitemap - Create sitemap.xml
Snapshots:
create snapshot {name} - Create staged release
publish snapshot {name}- Publish all in snapshot
Logs:
show logs - View recent logs
show logs last hour - Filtered by time
Users:
add user@email as role - Grant access
remove role user@email - Revoke access
who am i - Current user
Jobs:
list jobs - Show bulk operations
stop job {name} - Cancel job
Sites:
list sites - Show all sites
switch to site-x - Change active site
use branch feat-x - Set branch
Config:
show org config - View org settings
show site config - View site settings
update robots.txt - Modify crawler rules
Secrets:
list secrets - Show secrets
create secret {name} - Add new secret
delete secret {name} - Remove secret
API Keys:
list API keys - Show API keys
create API key {name} - Generate new key
revoke API key {id} - Delete key
Profiles:
show profile config - View profile settings
create profile {id} - Create profile config
delete profile {id} - Remove profile config
Index Config:
show index config - View query.yaml (index config)
update index config - Modify indexing rules
Sitemap Config:
show sitemap config - View sitemap.yaml (sitemap config)
update sitemap config - Modify sitemap rules
Versioning:
list versions - Show config history
restore version {id} - Rollback to version
Pages:
list pages - Show all indexed pages
list pages /blog - Filter by path prefix
Document Authoring (DA):
da auth - Authenticate with DA (IMS OAuth)
da list - List DA organizations
da list /path - List files in DA path
da source /path - Get file content from DA
da copy /src to /dest - Copy file/folder in DA
da move /src to /dest - Move/rename in DA
da delete /path - Delete from DA
da upload /path - Upload content to DA
da upload media /path - Upload image/media to DA
da config - View DA site config
da update config - Update DA site config
da versions /path - List file versions
da create version - Create labeled version snapshot
da restore version X - Restore a previous version
da preview /path - Preview DA content
da publish /path - Publish DA content
tools
Identifies which items (pages, campaigns, products, channels, regions) had the biggest increases or decreases for a key metric between two time periods. Use this skill when someone asks "what's up and what's down," "which campaigns moved the most," "top gainers and losers," "what pages are trending," "show me what changed by channel," or any variation of identifying the biggest movers and decliners for a metric.
tools
Compares the performance of two or more audience segments across key metrics side by side. Use this skill when someone wants to compare audiences, cohorts, or groups — for example, "how do mobile users compare to desktop users on conversion," "compare new vs. returning visitors," "show me the difference between these two segments," "compare these audiences on our KPIs," or "which segment performs better." Also trigger for "segment comparison," "audience comparison," or "cohort comparison."
business
Produces a compact KPI digest showing how key metrics changed over a period and what's driving the movement. Use this skill when someone asks for a performance summary, a weekly recap, a morning briefing, a KPI update, or any variation of "how did we do this week/month." Also trigger for requests like "give me a performance overview," "what moved in the last 7 days," "pull our KPI report," or "summarize our metrics."
testing
Analyzes a multi-step conversion funnel to find where users drop off and which steps have the worst leakage. Use this skill when someone describes a journey or funnel and asks about conversion rates, drop-off, fallout, or step completion. Trigger for phrases like "analyze our onboarding funnel," "where are users dropping off," "what's our checkout conversion rate," "funnel analysis," "show me fallout between these steps," or "which step loses the most users."