AI Gateway — Agent Skill
Manage Statamic content through a safe, authenticated tool execution gateway. Supports managing multiple Statamic sites from a single agent installation.
Before first use, follow the setup in INSTALL.md. For the endpoint contract, see references/api.md.
Site Registry
Credentials are stored in ~/.config/ai-gateway/sites.json (override with AI_GATEWAY_SITES_CONFIG).
{
"sites": {
"marketing": { "base_url": "https://marketing.example.com", "token": "token-aaa..." },
"docs": { "base_url": "https://docs.example.com", "token": "token-bbb..." }
}
}
Look up base_url and token by site name before every request.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /ai-gateway/execute | Execute a tool |
| GET | /ai-gateway/capabilities | List all tools and their enabled state |
| GET | /ai-gateway/capabilities/{tool} | Get full usage docs for a specific tool |
All requests require Authorization: Bearer {token}.
Discovery-First Workflow
Do not guess tool arguments. Always discover before executing:
GET /capabilities— see which tools are enabled on this siteGET /capabilities/{tool.name}— get the argument schema, validation rules, example request/response, allowed targets, denied fields, and behavioral notes for that tool- Use the returned information to construct your
/executerequest
This is the primary way to learn how to use any tool. The capabilities endpoints are the source of truth.
Request Envelope
{
"tool": "tool.name",
"arguments": { },
"request_id": "optional-tracking-id",
"idempotency_key": "optional-dedup-key",
"confirmation_token": "optional-if-confirming"
}
Response Envelope
Success: { "ok": true, "tool": "...", "result": { ... }, "meta": { ... } }
Error: { "ok": false, "tool": "...", "error": { "code": "...", "message": "..." }, "meta": { ... } }
Rules
⛔ CRITICAL — Structured field values are READ-ONLY structures. Bard, Replicator, Grid, and similar fields store values as deeply nested ProseMirror/TipTap JSON. When reading these back from
entry.getorglobal.get, you will see arrays of node objects withtype,attrs,content, andmarkskeys.You MUST NOT alter the structure. Never add, remove, reorder, or rename nodes, attributes, or marks. You may only change the literal
textstrings inside leaf nodes — nothing else.To update a rich-text field: (1) fetch with
entry.get/global.get, (2) change onlytextvalues, (3) send back structurally identical. Violating this corrupts content.
- Look up
base_urlandtokenfromsites.jsonbefore every request. - Discover before executing. Call
/capabilitiesthen/capabilities/{tool}before using any tool for the first time on a site. - Only call tools where
enabled: true. Only target allowlisted resources.forbiddenmeans off-limits. datamust be a JSON object, never an array or string. Don't send unknown argument keys.- Prefer
entry.upsertoverentry.create— safer and idempotent. navigation.updateis a full tree replacement. Always fetch withnavigation.getfirst.- Confirmation-gated tools require user approval. If
requires_confirmation: truein capabilities: (1) send the request, (2) receiveconfirmation_requiredwith a token, (3) show the user theoperation_summaryand ask permission, (4) only if approved, resend withconfirmation_token. Never auto-confirm. - If
rate_limited, back off and retry. - Include the site name in
request_id(e.g.marketing:upsert-about). - After bulk content changes, consider warming caches:
stache.warmrebuilds content indexes,static.warmregenerates static pages.
Error Codes
| Code | HTTP | Action |
|---|---|---|
unauthorized | 401 | Check token in sites.json |
forbidden | 403 | Target not in allowlist |
tool_not_found | 404 | Check name against /capabilities |
tool_disabled | 403 | Tool is off on this site |
validation_failed | 422 | Read error.message and error.details |
resource_not_found | 404 | Collection/entry/global/nav/taxonomy missing |
conflict | 409 | Entry exists — use entry.upsert |
rate_limited | 429 | Wait and retry |
confirmation_required | 200 | Resend with confirmation_token (after user OK) |
execution_failed | 500 | Retry or report |
internal_error | 500 | Retry or report |