Skills
Reusable Markdown procedures an agent loads on demand, so a long instruction lives in one file rather than in every prompt.
Skills live under .fabricharness/skills/<skill-name>/SKILL.md. They are Markdown-first: the body is the instructional prompt, the frontmatter declares metadata.
Format
---
name: triage-issue
description: Triage a GitHub issue and recommend severity, labels, and next action.
model: openai/gpt-5.5
---
You are triaging a GitHub issue.
Steps:
1. Read the issue using the `gh` command.
2. Inspect relevant files if available.
3. Determine severity.
4. Recommend labels.
5. Decide whether a fix can be proposed.Invocation
const result = await session.skill('triage-issue', {
args: { issueNumber: 42, repository: 'octocat/repo' },
commands: [gh],
result: schema.object({
severity: schema.enum(['low', 'medium', 'high', 'critical']),
summary: schema.string(),
recommendedLabels: schema.array(schema.string()),
fixSuggested: schema.boolean(),
}),
});The framework:
- Loads the skill by name.
- Interpolates
argsinto the prompt (template variables in the body, plus a structured arguments preamble). - Runs the harness loop with the chosen tools/commands.
- Validates the typed result.
Import a skill directly
A skill does not have to live under .fabricharness/skills/. Import its
SKILL.md and the whole directory compiles at build time:
import severityTriage from "../../skills/severity-triage/SKILL.md";
import { defineAgent } from "@fabric-harness/sdk";
export default defineAgent({
name: "incident-triage",
async run({ init }) {
const fabric = await init({ model: "openai/gpt-5.5", skills: [severityTriage] });
const session = await fabric.session();
return await session.skill("severity-triage", { args: { report } });
},
});The Markdown body becomes the prompt, the frontmatter becomes name,
description, model, and metadata, and every sibling file becomes a packaged
resource the model reads on demand — the same shape directory loading produces,
so an imported skill and a workspace skill are indistinguishable at runtime.
Importing buys two things over loading by name:
- Portability. The skill sits next to the code that uses it, so it can ship from a shared package instead of a per-project directory.
- Build-time failure. A wrong path fails the build; a wrong name in
session.skill('...')fails at model time, after the run has started.
Both forms are supported, and .fabricharness/skills/ is unchanged.
Markdown text imports
Any other .md import compiles to a string. Compose it with defineSkill()
when the file is not a SKILL.md:
import rubric from "../../skills/severity-triage/rubric.md";
import { defineSkill } from "@fabric-harness/sdk";
const closure = defineSkill({
name: "closure-summary",
description: "Summarize how an incident was closed out.",
instructions: `Summarize the closure in three bullets.\n\n${rubric}`,
});defineSkill() also accepts model, metadata, and packaged. A skill
carrying packaged registers that directory with the read tool, so every
resource it advertises is readable.
Type declarations
.md imports are typed by an opt-in ambient declaration, referenced once per
project:
/// <reference types="@fabric-harness/sdk/markdown" />fh init writes this file. It is opt-in rather than automatic because *.md is
a global wildcard that would otherwise collide with projects that already type
Markdown through Vite, MDX, or their own loader.
See examples/with-imported-skills for a
runnable version.
Why skills, not just prompts
- Reuse. Multiple agents can call the same skill.
- Versioning. Skills are tracked in source control and can be distributed as packaged skill directories.
- Auditability. Skill instructions are visible and reviewable, separate from agent code.
- Model overrides. A skill can declare its own model (for cheap classifiers, big-context reviewers, etc.).