Skill design patterns — three shapes most skills take¶
After you've built your first skill (see Write your first custom skill), the next question is: when I design my second one, what shape should it take? Most Reyn skills fall into one of three patterns. Pick by what the skill needs to do, not by complexity for its own sake.
Pattern 1: Linear (read → process → write)¶
Shape: phases connect in a single chain with no cycles and no branches. The LLM at each phase has exactly one allowed next phase.
When to use: each phase has a clear handoff to exactly one downstream phase. The work is essentially a pipeline — gather inputs, do the main processing, format and deliver.
Stdlib examples:
direct_llm— the simplest case: a single phase that finishes immediately. Graph:{ respond: [] }. No preprocessor, no branching, one LLM call.word_stats_demo— single phase plus a Python preprocessor that computes text statistics before the LLM call. Graph:{ review: [] }. Demonstrates the deterministic split principle inside a linear shape.read_local_files— two phases. Graph:{ decide_files: [read_and_respond], read_and_respond: [] }. The LLM atdecide_filesissues file-read ops, then transitions toread_and_respondfor the final answer.skill_importer— three-phase linear pipeline. Graph:{ search: [select], select: [convert], convert: [] }.
Trade-off: simple to reason about, but inflexible if the LLM produces output the next phase can't consume. There is no recovery loop — a bad output surfaces as a validation error and aborts.
Pattern 2: Loop (generate → review → refine)¶
Shape: one phase in the graph has more than one allowed next phase — it can transition forward (good enough → deliver) or back (needs work → refine again). The cycle is declared explicitly in the graph and is first-class:
Cycles in the skill graph are not a violation — they are a deliberate feature. See reference/dsl/graph.md for the syntax.
Loops can also be realized via OS rollback: a phase emits
control.type="rollback" and the OS re-runs an earlier phase with the
feedback injected into context. This avoids adding a back-edge to the graph
while preserving the iterative behavior. Both mechanisms produce the same
observable effect from the outside.
When to use: output quality varies; one pass is not reliably good enough; you want bounded retries based on judged quality (not just retries on validation error). Common in content generation, code generation, and iterative planning.
Stdlib examples:
skill_builder— five-phase linear graph with an OS-rollback loop. Graph:{ plan_skill: [design_artifacts], design_artifacts: [review_plan], review_plan: [build_skill], build_skill: [verify_skill] }. Theverify_skillphase runsreyn lint; if lint fails, it emits a rollback that re-runsbuild_skillwith the lint issues as feedback. The loop is bounded bymax_phase_visits.skill_improver— a longer loop skill. Graph:{ prepare: [copy_to_work], copy_to_work: [run_and_eval], run_and_eval: [plan_improvements], plan_improvements: [apply_improvements], apply_improvements: [finalize] }.apply_improvementsrolls back torun_and_evalfor the next iteration, or transitions tofinalizewhen a stop condition is met. Loop termination conditions (score threshold, max iterations, regression, stagnation) are documented in the skill'sskill.md.
Trade-off: powerful but cycles need a reliable finish path. Without a
termination condition the LLM can judge "needs more refinement" indefinitely.
Use phase.max_visits, skill-level iteration caps, or explicit stop conditions
to bound the loop.
Pattern 3: Sub-skill composition (delegation)¶
Shape: a phase invokes another skill as a sub-skill via the run_skill
Control IR op. The sub-skill runs to completion and its final_output artifact
flows back into the parent's workspace. The parent's graph does not change
shape — but one phase's allowed_ops includes run_skill.
Sub-skills can also be declared as graph nodes using the @sub_skill prefix.
See Compose skills with run_skill
for both flavors.
When to use: the work has a self-contained sub-task that is already a skill (or could become one), and you want to keep the parent's graph small. Also useful when the sub-task's output needs to be validated against an existing artifact schema before the parent proceeds.
Stdlib examples:
eval— therun_targetphase issues arun_skillControl IR op to invoke the target skill under test. Graph:{ run_target: [evaluate] }. Theallowed_ops: [run_skill]declaration onrun_targetis what permits this. The sub-skill's output is then passed to theevaluatephase for judging.skill_improver— therun_and_evalphase invokes theevalskill viarun_skill(documented inskill_improver/skill.md: "invokes theevalandeval_builderskills via therun_skillControl IR op").- Preprocessor
run_skill— a phase can also call a sub-skill deterministically before the LLM, in the preprocessor block. This is the form used byrecall_memoryin the chat router (see Compose skills with run_skill).
Trade-off: composition keeps each skill's graph simple and promotes reuse
of well-tested sub-skills. The cost is a runtime dependency: if the sub-skill
doesn't exist or its contract changes, the parent breaks. Validate with
reyn lint.
Mixing patterns¶
Real skills often combine two of the three patterns:
- Linear + sub-skill: a linear pipeline where one phase delegates to a
sub-skill.
evalis this shape — linear graph, one phase issuesrun_skill. - Loop + sub-skill: a loop skill where each iteration calls a sub-skill.
skill_improveris this shape — OS-rollback loop, withrun_and_evalcalling theevalsub-skill on every iteration. - Multi-agent (Layers 3 and 4) is orthogonal. Any of the three patterns can appear inside an agent that delegates to another agent. See multi-agent.md for the broader picture.
Combining all three patterns in one skill is a warning sign — it usually means the skill is doing too much.
Anti-patterns to avoid¶
-
Over-decomposition. Eight phases where three would do. Each phase boundary costs a context build. Default to fewer phases; split only when a phase has materially different instruction needs or a different input schema.
-
Cycle without a finish path. The LLM judges "needs more refinement" indefinitely. Always include a deterministic exit condition: a
max_phase_visitslimit, an iteration counter checked in phase instructions, or an explicit stop condition that transitions to a terminal phase regardless of quality. -
Sub-skill explosion. Making everything a sub-skill so the parent graph "looks clean." Sub-skills add lookup overhead and create dependency surface. Pre-existing reusable sub-skills are cheap to compose; speculative future-reuse sub-skills are not.
-
Branching without judgment. Graphs with
{ A: [B, C, D] }where the LLM cannot reliably tell which branch to pick. Branching makes sense when the input clearly distinguishes the path (see theskill_builderpattern table for an example of well-motivated branching). Avoid branches whose selection criterion is too subtle for the LLM to apply reliably.
See also¶
- principles.md — P1 and P2: the design-time invariants these patterns embody (Phase declares only input; Skill owns the graph).
- architecture.md — overall component layering.
- multi-agent.md — Layers 3 and 4, orthogonal to these three patterns.
- Write your first custom skill — apply these patterns in practice.
- Compose skills with run_skill — Pattern 3 in detail.
- reference/dsl/graph.md — graph syntax and cycle semantics.