---
audience: operators
summary: v1 platform-global, v2 partner-global, v3 opt-out (shipped 2026-07-03)
title: Global skills
path: modules/scaiskills/global-skills
status: published
---

# Global skills

Platform-global (and partner-global) skills reach every eligible scope's resolve response without any per-scope binding. Use for cross-cutting capabilities that every agent should have — a document-authoring skill, a support playbook, a compliance guide.

Shipped 2026-07-03. Three layers: **v1** platform-global, **v2** partner-global, **v3** per-scope opt-out.

## v1 — platform-global

A super_admin marks a skill global and pins the served version. From that moment forward every resolve response — for every tenant, every channel, every user — includes the skill.

```bash
curl -s -X PUT -H "Authorization: Bearer $SGK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "version": "0.1.3"
  }' \
  "$SCAIGRID_HOST/v1/modules/scaiskills/skills/scaiscribe/global"
```

**Response:**
```json
{
  "status": "success",
  "data": {
    "slug": "scaiscribe",
    "is_global": true,
    "global_version": "0.1.3",
    "global_scope_type": "platform",
    "global_scope_id": null,
    "updated_at": "2026-07-03T10:15:22Z"
  }
}
```

### Version pinning

Global skills always serve the pinned version — never `latest`. This is deliberate: a bad release can't silently propagate to every tenant.

- Yanking the pinned version un-globals the skill until a super_admin re-pins.
- Deprecating the skill has the same effect.
- Only one non-yanked published version per skill can be pinned at a time (enforced in a single transaction).

To move to a new version, call `PUT .../global` again with the new `version` — the old pin is cleared and the new one set atomically.

### Clearing

```bash
# Either shorthand:
curl -s -X DELETE -H "Authorization: Bearer $SGK_TOKEN" \
  "$SCAIGRID_HOST/v1/modules/scaiskills/skills/scaiscribe/global"

# Or full body:
curl -s -X PUT -H "Authorization: Bearer $SGK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}' \
  "$SCAIGRID_HOST/v1/modules/scaiskills/skills/scaiscribe/global"
```

### Body-strict schema

The request body uses `additionalProperties: false`. A misdirected field returns 422 with the offending field named — deliberately prevents the "silently-ignored path" class of bug:

```bash
curl -s -X PUT -H "Authorization: Bearer $SGK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true, "version": "0.1.3", "path": "typo"}' \
  "$SCAIGRID_HOST/v1/modules/scaiskills/skills/scaiscribe/global"
# → 422 { "error": { "code": "VALIDATION_FAILED", "detail": "path: Extra inputs are not permitted" } }
```

## v2 — partner-global

Same route, `scope_type: "partner"` + `scope_id: "<partner_id>"`. Reaches only tenants under the specified partner.

```bash
curl -s -X PUT -H "Authorization: Bearer $SGK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "version": "0.1.3",
    "scope_type": "partner",
    "scope_id": "prt_abc123"
  }' \
  "$SCAIGRID_HOST/v1/modules/scaiskills/skills/vat-verification/global"
```

**Authority:**
- `super_admin` — can set platform or partner scope for any partner.
- `partner_admin` — can set partner scope for their OWN partner (`scope_id` must equal the caller's `partner_id`).
- Any other role — 403.

**Response includes the scope:**
```json
{
  "status": "success",
  "data": {
    "slug": "vat-verification",
    "is_global": true,
    "global_version": "0.1.3",
    "global_scope_type": "partner",
    "global_scope_id": "prt_abc123",
    "updated_at": "2026-07-03T10:22:07Z"
  }
}
```

## v3 — per-scope opt-out

Tenants (or users, or channels) that don't want a specific global can opt out. The global stays live for everyone else; the opting-out scope just stops seeing it.

```bash
curl -s -X PUT -H "Authorization: Bearer $SGK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "scope_type": "workspace",
    "scope_id": "ws_our_tenant",
    "reason": "conflicts with our internal doc-authoring skill"
  }' \
  "$SCAIGRID_HOST/v1/modules/scaiskills/skills/scaiscribe/global/opt-out"
```

**Authority:**
- `super_admin` — unrestricted.
- `tenant_admin` / `partner_admin` — can opt out for their OWN workspace.
- Any user — can opt out for their own `user` scope.
- `channel` / `core` scope opt-outs require the `scaiskills:manage` module permission.

**Idempotent.** Hitting the same `(slug, scope)` twice returns the same row.

**Reversing:**
```bash
curl -s -X DELETE -H "Authorization: Bearer $SGK_TOKEN" \
  "$SCAIGRID_HOST/v1/modules/scaiskills/skills/scaiscribe/global/opt-out?scope_type=workspace&scope_id=ws_our_tenant"
# → { "status": "success", "data": { "removed": true } }
```

Returns `{"removed": false}` (still 200) if there was no opt-out to remove — safe to run in idempotent playbooks.

**Opt-outs affect both `/resolve` AND `/mcp/skills/view`** — a scope can't hide a global from resolve and still fetch its `SKILL.md` via view. Same guard.

## Resolve response — the `source` field

Every resolved skill now carries a `source` field so consumers can badge globals in the preamble:

```json
{
  "status": "ok",
  "data": {
    "skills": [
      {
        "slug": "scaiscribe",
        "version": "0.1.3",
        "description": "Universal document-authoring skill.",
        "triggers": ["docx", "pptx", "xlsx"],
        "source": "global"
      },
      {
        "slug": "our-legal-template",
        "version": "1.0.0",
        "description": "Company-specific legal boilerplate.",
        "triggers": ["contract", "NDA"],
        "source": "binding"
      }
    ],
    "cache_ttl_ms": 60000
  }
}
```

Consumers may render "🌐 Global" badges next to `source: "global"` entries so agent authors know the skill isn't per-tenant configured.

## Precedence

If a scope has BOTH a global and a binding for the same slug, the binding wins. Full order (highest wins):

    core > user > channel > workspace > global

This lets a tenant "pin" a different version of a global skill — bind their preferred version to `scope_type: "workspace"` and it overrides the global for their tenant only.

## Cache behavior

Global-flag changes affect every scope's resolve result. Instead of scanning and invalidating every cache key, the resolver keys its Redis cache under a versioned prefix that bumps on any `set_global` / `clear_global` / `opt_out` / `opt_in`:

    scaiskills:resolve:v{N}:{scope_type}:{scope_id}:p{partner_id_or_-}

Old keys (`v{N-1}...`) expire naturally at their 60 s TTL. Effect: a super_admin's `PUT .../global` shows up in every tenant's next resolve within 60 seconds worst case, immediately for scopes that resolve before their old cache expires.

## Catalog fields

`GET /v1/modules/scaiskills/skills` and `GET /skills/{slug}` now carry three new fields per row so admin UIs can render "Make global" / "Global" state:

- `is_global` (bool)
- `global_version` (semver or `null`)
- `global_scope_type` (`"platform"` | `"partner"` | `null`)

## Audit

Every set / clear / opt-out / opt-in writes an audit row with `action` set to one of:

- `scaiskills.global.set`
- `scaiskills.global.clear`
- `scaiskills.global.opt_out`
- `scaiskills.global.opt_in`

Filter the audit log with `?action=scaiskills.global.set` to see everything that's been made global platform-wide.
