TechFabricTechFabricHarness
Building Agents

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:

  1. Loads the skill by name.
  2. Interpolates args into the prompt (template variables in the body, plus a structured arguments preamble).
  3. Runs the harness loop with the chosen tools/commands.
  4. 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:

.fabricharness/fabric-env.d.ts
/// <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.).