---
summary: Common symptoms and what they usually mean.
title: Troubleshooting
path: troubleshooting
status: published
---

# Troubleshooting

A short list of things that go wrong and how to fix them. If none of these match, check the request id in the response envelope and grep the ScaiGrid logs.

## Publish returns `SCAISKILLS_VALIDATION_FAILED`

The validator's `context.errors` array names the exact stage that failed. Common ones:

- **Slug mismatch.** The `name` field in `SKILL.md`'s frontmatter must equal the URL slug. A common copy-paste error after renaming.
- **Non-monotonic version.** The `version` in the manifest must be strictly greater than every previously published version for this skill. Republishing `0.1.0` after `0.2.0` is rejected; bump to `0.2.1`.
- **Missing `SKILL.md`.** The bundle has to contain `SKILL.md` at the tar root (or under a single top-level directory). Bundling from inside the directory (`tar -czf x.tar.gz *`) usually works; bundling the directory itself can put the file one level too deep.
- **Frontmatter not parsed.** Make sure the file starts with `---` on its own line and the closing `---` is also on its own line.
- **References path escape.** `references/../etc/passwd` or absolute paths in the references directory are rejected.

## Publish returns `SCAISKILLS_VERSION_CONFLICT`

The semver you uploaded already has a published row for this skill. Even if the bundle bytes are identical, the version row is unique on `(skill_id, semver)`. Bump the semver in `SKILL.md` and re-tar.

## Binding stays in `pending_grants` after a grant call

The grant flow only clears the permissions axis. If `pending_grants` won't flip even after granting every declared permission, the binding still has unmapped required secrets. Required secrets are supplied at bind time only — there's no endpoint to add a missed one. Delete the binding and recreate it with complete `secret_mappings`.

Less commonly: a permission string was mistyped on the grant call. The match is byte-equal against the manifest's `permissions:` list. Compare them side by side.

## Resolve returns an empty `skills` list

In rough order of frequency:

- **No bindings for the scope.** `GET /bindings?scope_type=workspace&scope_id=...` should show at least one row.
- **All bindings are `pending_grants: true`.** They're filtered from resolve. Clear the gates.
- **All bindings are `enabled: false`.** They're filtered too — check the `enabled` flag on each row.
- **Wrong scope id.** ScaiWave channel ids and ScaiGrid room ids are not the same string in every deployment; confirm what the runtime is actually sending.

## Binding to `@latest` resolves to an old version

The resolver picks the highest published, non-yanked version at bind time. If a newer version was published *after* the bind, the binding won't see it — floating refs do not re-resolve. Delete and re-create the binding to roll forward, or bump to a pinned version.

If you genuinely expected a newer version to exist, double-check it isn't sitting in `status: "draft"` or was yanked.

## `skills.view` returns `SCAISKILLS_SKILL_NOT_FOUND` for a slug that exists

`view` enforces scope gating: the slug must appear in the caller's resolved set. If the caller's scope has no active binding for the skill, view returns 404 even when the skill itself is published. Check the resolve output for the same scope first.

## Yanked version still showing up in `latest` resolution

Yanked versions are skipped by `latest` and floating refs on subsequent binds. Existing bindings that resolved to the yanked version *before* the yank keep working — yank is non-disruptive by design. If you need them gone, delete and re-bind.

## Bundle upload returns 413 or hangs

The bundle is too large or the upstream is dropping the request. Check the `SCAISKILLS_BUNDLE_TOO_LARGE` cap in your deployment's settings. If you're below the cap, the file is genuinely too large for the path — split the references into separate skills, or store reference content in ScaiDrive and read it from inside `SKILL.md` instead of bundling it.

## Search returns nothing or wrong results

`skills.search` runs through ScaiMatrix first; on failure it falls back to a substring filter over the resolved set. Empty results usually mean:

- **ScaiMatrix not configured.** `settings.weaviate_url` empty → no semantic index, substring fallback only. Set the URL or accept the fallback.
- **Out-of-scope match.** The semantic search returned matches but they're not in the caller's resolved scope, so they're stripped. Confirm the binding for the expected slug exists in this scope.
- **Index lag.** Indexing runs best-effort at publish time. A freshly published skill may not be in the index for a few seconds. The substring fallback works in the meantime.

## Cache feels stale after editing a binding

Every write to the binding table invalidates the relevant scope's cache key. If you're still seeing the old answer after a known-good write, three usual causes:

- **Wrong scope key.** You wrote to `(channel, chn_xyz)` but the runtime is resolving against `(workspace, ten_xyz)`. Resolution merges scopes but caching is per-primary-scope. Invalidate by issuing a write against the primary scope, or wait 60 seconds.
- **Redis unavailable.** Cache writes silently fail; reads silently miss. The next resolve hits MariaDB directly — usually self-healing.
- **Long-lived runtime client.** If the runtime caches results locally on top of ScaiGrid's cache, the runtime's local TTL dominates. The 60 s ScaiGrid TTL is the floor, not the ceiling.

## ScaiCore picks up the wrong skills at boot

ScaiCore resolves at start with `scope_type: core`, and the resolved set is pinned for the Core's lifetime. If the Core is showing stale skills:

- Check whether the Core is being restarted at all on config change — pinning is by design, so restarts are how rollouts happen.
- For `required` skills declared in the Core's metadata, a missing or yanked binding fails the start outright. A `required: false` skill missing just warns and continues.

## Dependency resolution fails with `SCAISKILLS_UNRESOLVABLE_DEPENDENCY`

A `requires.skills` ref in the bundle didn't match any published, non-yanked version. Most often this is a slug typo in the depending skill's manifest, or the dependency skill itself was yanked across every version that satisfies the range. Check the depending bundle's `manifest.requires.skills` against `GET /skills/{slug}` for the dependency — the published versions array should show at least one match for the range.

## A bind succeeds but `resolved_deps_json` is empty

That's not necessarily wrong — empty means the skill declared no `requires.skills`. The lockfile only contains transitive dependencies, not the root skill itself (the root is identified by the binding row's own `skill_id` and `resolved_version`). To confirm the dependency list was actually empty, look at the skill version's manifest in the catalog.
