A skill+ is a skill, plus an optional
flowand an optional set ofrules. Concretely: an Agent Skills directory with aSKILL.md, optionally extended with a flow (a stage outline) and arules/folder.This document is the format spec (directories / files / fields / constraints). For why it's designed this way and the conceptual execution model →
design.md. For how to use it and a worked example →../README.md.Modeled on the Agent Skills specification. Status: v0 draft; 🚧 marks open questions.
skill-name/
├── SKILL.md # REQUIRED — the skill itself (frontmatter + body)
├── flow.py # optional — the flow (stages + state) as a Python eDSL (§3)
├── rules/ # optional — soft & hard rules
├── references/ # optional — knowledge files (a when: makes one a tip)
├── scripts/ # optional — callable tool scripts
└── assets/ # optional — templates / static resources
SKILL.md is the only required file — on its own it is a valid plain skill. Adding a flow and/or rules/ makes it a skill+; remove those and it degrades back to a plain skill (skill+ ⊇ skill). Routing uses the same mechanism as skills (§9).
YAML frontmatter + Markdown body. Fully compatible with the Agent Skills SKILL.md.
| Field | Required | Constraint |
|---|---|---|
name |
yes | 1–64; lowercase letters / digits / hyphens; no leading/trailing or doubled hyphens; matches the parent directory name |
description |
yes | 1–1024, non-empty; says what it does + when to use it (keywords for routing) |
license |
no | license name or reference |
compatibility |
no | ≤500; environment requirements (target product / system deps / network) |
metadata |
no | arbitrary string k-v |
allowed-tools |
no | space-separated pre-approved tools (experimental) |
Illegal name examples: PDF-x (uppercase), -x (leading hyphen), x--y (doubled hyphen).
The Markdown after the frontmatter: the opening paragraph is the brief (a one-line thesis / plan, always resident); the rest is the skill's resident, unconditional knowledge. Situation-specific knowledge does not go here — it goes in references/ with a when: (§5). Keep it under ~500 lines; push detail to references/ (§7, progressive disclosure).
A flow declares the stages a task moves through (and, optionally, a typed state). It exists so a runtime can render a "map + you-are-here", and so knowledge/rules can be scoped to a stage. The flow is a map the model reads, not a program an engine executes (see design.md).
flow.pyis provisional. A small Python eDSL is how this spec expresses the idea for now — it is not a claim that a Python eDSL is the right authoring surface. The flow is fundamentally an IR; the eDSL is just one front-end (a markdown outline, a YAML topology, or a more natural-language surface could compile to the same IR). The right surface is an open question (seedesign.md§6/§7). The primitives below define the IR's shape, not a final syntax.
Declare the flow as a small Python eDSL. build(flow) declares an IR at build time (it does not run business logic); state is a State subclass (§4).
from skillplus import Flow, State
class MyState(State): # domain state (§4)
...
def build(flow: Flow): # declarative — builds the IR, does not execute
flow.stage("load-profile", tool=scripts.get_profile)
with flow.branch(lambda s: not s.profile_fresh):
flow.stage("kyc", rule=rules.suitability)
flow.ask("ask-preference", "ask sector / horizon / ESG")
with flow.loop("screen", until=lambda s: len(s.candidates) <= 3, max=4):
flow.stage("filter", tool=scripts.apply_filters)
with flow.on_failure():
flow.stage("relax", "nothing left → loosen one threshold and retry")
with flow.fanout("deep-dive", over=lambda s: s.candidates):
flow.agent("analyze", prompt="research {item.ticker}: fundamentals + news + risk",
verify="must give both bull and bear case")
flow.stage("recommend", rule=rules.disclaimer)| Primitive | Form | Meaning |
|---|---|---|
flow.stage(name, desc="", *, tool=, rule=) |
leaf | one step; tool= = spotlight (which scripts matter here, not an availability switch); rule= = bind a local rule |
with flow.branch(cond) / with flow.otherwise() |
block | branch; cond = predicate lambda s or description string; successive branch = ordered first-match, otherwise = else |
with flow.loop(name, *, until, max) |
block | iterate; termination = until (predicate / description) + max (hard cap) |
with flow.fanout(name, *, over) |
block | parallel fan-out; over = predicate returning an iterable / description; {item} available in prompts |
with flow.on_failure() |
block | fallback for the nearest enclosing step/block |
flow.agent(name, *, prompt, verify=None) 🚧 |
leaf | spawn a sub-agent worker; verify = exit gate |
flow.ask(name, question) |
leaf | HITL input: pause, ask, continue with the answer |
flow.confirm(name, question) |
leaf | HITL approval: pause for sign-off (a manual gate) |
Two-form conditions: branch / until / fanout.over take either a predicate lambda s: (evaluated against typed state — mechanical, reliable) or a description string (judged by the model — a fuzzy fallback); prefer a predicate when it can be decided mechanically, especially for until. prompt / question are always description strings.
🚧 Progress signal (open, central): under guided execution, how does the model tell the runtime that a
stageis done / which description branch it took / that a descriptionuntilhas converged, so the position can advance? The channel is undecided (seedesign.md). Predicate conditions the runtime evaluates itself; description conditions and stage-completion depend on this signal.
A runtime only ever consumes the flow IR;
flow.pyis one front-end that compiles to it. Rationale for the design indesign.md.
When using flow.py, declare a pydantic class extending the base State (class MyState(State)). The base carries the mechanics; the subclass declares only domain fields.
- The base provides: identity + persistence (a conversation-scoped key, a state store); per-field merge / reducers (list dedup / dict per-key / scalar conflict fail-closed;
Annotated[list, reducer]to override); read-only flow position (s.stage/s.round); a validation anchor forwhen:and condition predicates. - The base holds no domain fields (pushing domain semantics into the base = overfitting).
- Store only "facts that can't be re-derived" + internal position; external-world facts (state of external systems, etc.) are read live at decision time, not cached as authority (rationale in
design.md).
state drives two things at once: ① the flow's current position, and ② the when: triggers of references/rules.
🚧 Write API (open, central): how the outputs of a tool / worker /
flow.ask/flow.agentland as typed fields — the convention is "return a patch, merge by field name", but NL-output → field,fanoutresults →dictby key, and which fieldask/agentwrites are undecided (seedesign.md).
Markdown knowledge files. Frontmatter with a when: → injected into context when it matches (a tip); without a when: → deep knowledge loaded on demand. Keep them flat and small.
Scripts (Python / Bash / …), always in the tool list, callable in any turn; a flow's tool= only spotlights, it does not change availability. Self-contained or with stated deps; give clear errors. May be referenced by "module:symbol".
Each rule has two orthogonal axes:
on_fail(soft / hard):advise→self-check→ask→deny→stop; the first two are soft (never block), the last three are hard (gate / stop).- Delivery (file extension):
.md= injected into context, the model reads it (trust the model);.py/.sh= a host hook, machine-enforced. The two axes are independent (a.mdcan be a hard rule, a script can be a soft rule).
The source of truth is the file's frontmatter / header fields (on_fail + optional when). Filenames carry no semantics (optional rules/hard/, rules/soft/ subdirs are pure organizational sugar). Binding: globally via its own when:, or locally by a flow step's rule=. Multiple rules: soft = collect, don't block; hard = a single decisive verdict.
Templates / images / data files, same as Agent Skills; referenced on demand by SKILL.md / references / scripts, never resident in context.
The trigger condition for references (tips) and rules: a structured predicate + an optional natural-language note. Predicate fields come from a namespace union (not just state fields):
when: stage=kyc # flow position (runtime-maintained, §4)
when: candidates_left<=3 # domain state field (validated against the schema)
when: tool=push@prod # tool-call context (guarding a tool, §8)
when: market=volatile # external / derived fact ("read live, not stored", §4; namespace is open)
when: field source |
Example | Validation |
|---|---|---|
| domain state field | risk_level=R5 |
against the State schema |
| flow position | stage= / round= |
against flow node names |
| tool-call context | tool= / @env |
hard rules guarding tool use (§8) |
| external / derived fact | market= … |
open, not strictly validated (external facts read live) |
No bare prose (parsing drifts), no bare code (over-promises an execution engine). 🚧 The predicate grammar (equality / range / set / @env) is undecided.
v0 / v1: v0 = the declarative predicates above (
stageis the workhorse; domain fields / external facts follow); arbitrary logic does not go into a markdownwhen:— it lives on the Python side (aflow.pylambda sor a.pyrule'scheck). v1 = richer grammar + alambda whenfor markdown tips. Seedesign.md.
| Layer | Content |
|---|---|
| Resident | description (routing) + brief + SKILL.md resident knowledge + the flow map (with current position) |
Enhanced (by when:) |
matched tips (references) + soft rules (rules/*.md) + the current stage's spotlighted tool |
| On demand | references deep knowledge without a when:, script output |
Compare Agent Skills: description (~100 tokens) → SKILL.md + flow → tips / soft rules → references. 🚧 Render format / token budget undecided (design.md).
- Soft (
advise/self-check): inject text, the model self-checks, never a machine block. - Hard (
ask/deny/stop) andflow.ask/flow.confirm: can gate / can stop. A.pyrule'scheck(ctx)reads live truth at decision time (not the cached state used for injection).
Which host seam a rule lands on is an implementation detail (see design.md). The author only writes on_fail / when or flow.ask/confirm, and never touches a hook API.
A runtime scans each directory's SKILL.md frontmatter (static — it does not execute flow.py) and matches on description. A matched entry that has a flow → structured execution (skill+); without one → a plain skill. No new registry is needed — an existing Agent Skills registry is extended to recognize "a skill directory that also has a flow / rules/" as a skill+. Execution model in design.md.
skillplus validate <dir>: checks the frontmatter and name rules; that the flow builds a valid IR; that rules/ have a legal on_fail; that every when: field belongs to a known namespace (state fields against the schema; stage/round against flow nodes; tool/external facts per their source, external facts not strictly validated, §6); that scripts/ and rules/*.py load — mirroring the Agent Skills validator.