# Skills in Workflows
> Declare the skills a phase needs, bundled in your plugin or pinned to a repository, and have them registered and injected automatically.
A skill is a folder containing a `SKILL.md` file with YAML frontmatter, following
the [vercel-labs/skills](https://github.com/vercel-labs/skills) convention. It is
the unit of capability a workflow declares for the agent running a phase.
Skills are harness-agnostic. The same declaration works whether a phase runs
Claude or Codex, because the pinned `skills` CLI inside the workspace knows where
each harness looks.
## Two layers, and why it matters
Capability in a Syntropic137 workspace comes from two places. Knowing which is
which saves confusion later.
| | Platform layer | Workflow layer |
|---|---|---|
| **What** | Hooks, commands, guardrails | Skills |
| **Where it lives** | Baked into the workspace image at `/opt/agentic/plugins/` | Declared in your workflow YAML |
| **Who owns it** | Us, as we build the platform | You, the workflow author |
| **Changes when** | The image is rebuilt | You edit a workflow |
| **Harness** | Claude-specific (`--plugin-dir`) | Any harness |
The platform layer carries things skills cannot express. A hook fires on an
event; a skill is instructions an agent reads. Observability capture and
dangerous-command guardrails are hooks, so they ship with the image and version
with the harness they steer. You do not declare them, and you cannot break them
from a workflow.
Everything on this page is the workflow layer.
If a capability ever needs to change without an image rebuild, that is a signal
it is not platform-level and belongs here as an ordinary workflow skill.
## Declaring skills
`skills:` is accepted at workflow scope and at phase scope. Workflow scope
applies to every phase; phase scope is additive on top of it.
```yaml
id: review-workflow
name: Review Workflow
type: review
skills:
- ./skills/repo-conventions # bundled in this plugin
- anthropics/skills/frontend-design@v1.2.0 # pinned external ref
phases:
- id: review
name: Review
order: 1
skills:
- acme/skills/tdd-workflow@v2.0.0 # only this phase
```
When the same skill is declared at both scopes, the phase-scope entry wins. There
is no silent last-one-wins across different versions: two conflicting versions of
the same skill name abort the run rather than picking one.
## The three reference forms
**Bundled** - a path inside your plugin, relative to its root:
```yaml
skills:
- ./skills/repo-conventions
```
**Shorthand** - `org/repo/skill-name@version`. The third segment names the skill
folder inside the repository, which matters because a skills repo usually
publishes several:
```yaml
skills:
- anthropics/skills/frontend-design@v1.2.0
```
**Verbose** - for full URLs, several skills from one source, or a version
containing a slash:
```yaml
skills:
- source: github.com/acme/agent-skills
version: feature/new-review-flow
names: [code-review, tdd-workflow]
```
## Pinning is required
`@latest` is rejected. This is not style enforcement.
Registration is content-addressed: a skill is stored under the sha256 of its file
tree, and that hash is the cache. A reference that resolves to an
already-registered hash does no network work at all, which is what makes repeated
installs fast. An unpinned reference cannot be cached honestly, because the same
reference may mean different bytes tomorrow.
So pinning is what makes the cache sound, not merely what makes runs reproducible.
### How bundled skills get a version
An external reference carries its own version. A bundled path does not, so the
CLI pins it by the sha256 of its file tree and rewrites the reference before
uploading.
The practical consequence: **editing a bundled skill produces a new
registration.** That is exactly true, and it is deliberate. A fixed label would
mean an edited skill silently resolved to the previously stored copy, which is
the failure this design exists to avoid.
## Installing
`syn workflow install` registers every declared skill before creating any
workflow:
```bash
syn workflow install ./my-plugin
```
```
Resolving 1 skill(s)...
registered skill repo-conventions@sha256-87701f89...
[1/1] Creating Skills Demo... done (id: skills-demo)
```
Run it again and the hash is already stored, so nothing uploads:
```
skill repo-conventions@sha256-87701f89... already registered
```
A bad or unreachable reference fails the **install**, and no workflow is created.
That is the point of doing this at install time: a typo surfaces before you have
committed to a run, not in the middle of one.
Installing a package whose workflow id already exists currently fails with a
concurrency error. See
[#822](https://github.com/syntropic137/syntropic137/issues/822). Skills are
unaffected: the preflight reports them as already registered first.
## Inspecting what is registered
```bash
syn skill list # everything registered
syn skill show repo-conventions # every version of one name
syn skill add ./path/to/skill # register without a workflow install
```
`list` and `show` take `--json` for scripting and agent consumption.
`show` returns every registration sharing a name, because a name is not unique:
the same skill can be pinned at several versions, and two sources can publish the
same name. Seeing all of them is how you tell which pin a workflow resolves to.
## What happens at run time
1. The phase's skills are resolved against the lock: workflow scope merged with
phase scope, keyed by `(source_url, version, skill_name)`.
2. Each resolved tree is materialized into the workspace at
`/workspace/.syn-skills//`.
3. During setup, the pinned `skills` CLI installs each one for that phase's
harness, offline, from the local path.
Step 3 is the one that matters. Files under `.syn-skills/` are staged, not
installed. You can confirm the difference inside a running workspace:
```bash
docker exec skills list --agent codex
```
```
Project Skills
repo-conventions ./.agents/skills/repo-conventions Agents: Codex
```
A phase that declares skills but cannot install them **fails** rather than
running without them. Silently proceeding would produce a plausible-looking run
against the wrong capabilities, which is worse than a clear failure.
## Storage
Skill storage is content-addressed and grows monotonically. Eviction is
deliberately not implemented, so size is reported rather than assumed small:
```bash
curl "$SYN_API_URL/skills/storage"
```
```json
{"object_count": 2, "total_bytes": 302, "skill_count": 1, "truncated": false}
```
`skill_count` counts distinct trees, not registrations, so two names over
identical content correctly report as one stored tree.
## Reference
- [ADR-065](https://github.com/syntropic137/syntropic137/blob/main/docs/adrs/ADR-065-claude-plugin-injection.md) - the decision record and its rationale
- [#772](https://github.com/syntropic137/syntropic137/issues/772) - the specification
- [Claude Plugins](/docs/guide/claude-plugins) - the older, Claude-only workflow-declared mechanism that skills replace