Building a skillpack

Build the smallest complete skillpack — hello-world — from scratch. A deterministic skill, an agentic skill, a custom tool, and two subagents, end to end.

This guide builds a skillpack from scratch — the smallest one that still uses every core piece. By the end you'll know what a skillpack is, the two kinds of skill, how a skill declares its contract, and how skills call tools and each other. Read skill-yaml-reference for the field-by-field manifest spec; this guide is the shape and the why.

The example is hello-world: give it a name and it builds a little greeting card. No external APIs and no media, so it's cheap to run and easy to read.

The finished pack is public at github.com/PurasAI/hello-world — clone it, or scaffold it straight into a fresh directory with puras init --template hello-world. Starting your own pack instead? Default puras init (or github.com/PurasAI/skillpack-template) gives you the blank version of the same layout.

What a skillpack is

A skillpack is a directory of skills. Each top-level folder that contains a skill.yaml is a skill — discovered by scanning the bundle root. There's no skills/ wrapper and no root manifest: a skill's name is its folder name.

hello-world bundles two skills, one of each kind:

SkillKindEntrypointWhat it shows
formatterdeterministicscripts/format.py:runa plain Python skill — no LLM
greeteragenticSKILL.mda custom tool + two subagents, driven by a model

The two compose: greeter calls formatter to lay out its card. A skillpack is a pipeline of related skills, not a junk drawer.

hello-world/                  # the bundle root
├── formatter/                # each top-level dir with a skill.yaml is a skill
│   ├── skill.yaml            # manifest: schemas (deterministic skill)
│   └── scripts/format.py     # run(name, shout, poem) -> {card}
└── greeter/
    ├── skill.yaml            # manifest: schemas, model, the `emphasize` tool
    ├── SKILL.md              # the agent's system prompt (the entrypoint)
    ├── tools/emphasize.py    # custom tool — run(text) -> {loud}
    └── references/poet.md    # a subagent's prompt, read on demand

The entrypoint suffix decides the kind: a Python file.py:func entrypoint is deterministic; a markdown SKILL.md entrypoint (plus a text_model) is agentic. Everything else is the same skill.yaml contract.

Part 1 — A deterministic skill (formatter)

The simplest skill is a Python function with a typed contract. formatter takes a name (plus an optional shout and couplet) and returns a text card. Its skill.yaml is just the contract — no model, no SKILL.md:

yaml
title: Greeting Formatter
description: Lays out a small greeting card from a name.

entrypoint: scripts/format.py:run

input_schema:
  type: object
  required: [name]
  properties:
    name:  { type: string, maxLength: 60 }
    shout: { type: string, maxLength: 120 }
    poem:  { type: text,   maxLength: 400 }

output_schema:
  type: object
  properties:
    card: { type: text, description: The assembled greeting card. }

The entrypoint scripts/format.py:run points at a function. The worker calls it as run(**inputs) and validates the returned dict against output_schema:

python
def run(name: str, shout: str = "", poem: str = "") -> dict:
    headline = (shout or f"Hello, {name}!").strip()
    # ...frame the headline + couplet in a text box...
    return {"card": "\n".join(rows)}

That's a whole skill. A deterministic skill is code with a schema — use one whenever the work is mechanical and needs no judgment. Pure stdlib here, so there's no requirements.txt; add one next to the script if you need packages.

Part 2 — An agentic skill (greeter)

greeter does need judgment (writing a greeting), so it's agentic: its entrypoint is SKILL.md and it declares a model. Its skill.yaml adds a text_model, a richer input_schema, and a custom tool:

yaml
title: Greeter
entrypoint: SKILL.md
text_model: claude/haiku-4-5

input_schema:
  type: object
  required: [name]
  properties:
    name:  { type: string, maxLength: 60 }
    style: { type: string, enum: [friendly, formal, playful], default: friendly }

output_schema:
  type: object
  properties:
    card:  { type: text }
    shout: { type: string }
    poem:  { type: text }

tools:
  - name: emphasize
    description: Make a short string LOUD.
    entrypoint: tools/emphasize.py:run
    input_schema:
      type: object
      required: [text]
      properties:
        text: { type: string }
    output_schema:
      type: object
      properties:
        loud: { type: string }

A few things to notice:

  • The model is set high-level, in text_model: — not hard-coded in a prompt. greeter is a thin orchestrator, so it runs claude/haiku-4-5; spend frontier models where real judgment lives. (See skill-yaml-reference for media model slots like image_model: too.)

  • The schema dialect drives the UI. style's enum becomes a dropdown in the playground; type: text becomes a multi-line field. Richer types (image, video, color) render upload widgets and pickers — a well-typed schema means the playground form "just works" with zero UI code.

  • A custom tool is a deterministic Python function, declared under tools: with its own input_schema/output_schema and a file.py:func entrypoint — exactly like a deterministic skill, but callable by this agent:

    python
    # tools/emphasize.py
    def run(text: str) -> dict:
        return {"loud": text.upper().rstrip("!?. ") + "!!!"}
    

SKILL.md is the brain

skill.yaml is the contract; SKILL.md is the system prompt the agent runs. greeter's walks through three small moves, then returns. It uses the tool and two subagents rather than doing the work itself:

markdown
## Step 1 — Shout the name (custom tool)
emphasize({ "text": <name> })          → keep `loud` as your `shout`

## Step 2 — Ask the poet (a `.md` subagent)
run_subagent({ "target": "references/poet.md",
               "inputs": { "name": <name>, "style": <style> } })   → keep `poem`

## Step 3 — Assemble the card (a sibling skill as a subagent)
run_subagent({ "target": "formatter",
               "inputs": { "name": <name>, "shout": <shout>, "poem": <poem> } })

## Step 4 — Return
set_output({ "card": ..., "shout": ..., "poem": ... })

Subagents — two shapes

run_subagent hands a self-contained stage to a fresh agent (its own context, linked to this run). greeter uses both forms a skillpack offers, resolved inside this same bundle:

  • A .md prompttarget: "references/poet.md" runs that bundle file as the system prompt of an isolated subagent. Keep a stage's prompt in references/ and point at it.
  • A sibling skill by nametarget: "formatter" runs another skill in this skillpack. (Cross-skillpack refs use skillpack/skill or workspace/skillpack/skill; an inline prompt: runs a one-off with no file.)

inputs is passed to the child verbatim — it reads them as its own inputs. This is how a skillpack becomes a pipeline: each skill stays small and one agent stitches them together.

Finishing a run — set_output

Every skill ends by calling set_output once, with exactly the fields in its output_schema. For agentic skills it's an auto-injected tool; for deterministic ones it's the returned dict. The platform validates the result against the schema and prunes anything extra — so output_schema is the real boundary of what a skill exposes.

Seed the playground with examples

Each skill.yaml carries an examples block — real inputs the playground loads as one-click starting points. greeter ships a couple:

yaml
examples:
  - title: Greet Ada
    inputs: { name: Ada, style: playful }
  - title: Greet the team (formal)
    inputs: { name: the Puras team, style: formal }

Good examples are how a new user understands a skill in five seconds — invest in them, and make sure they mirror the real input schema.

Deploy and run it

A skillpack is deployed as a whole bundle, versioned per skillpack. Scaffold a new one and push it with the CLI:

bash
pip install puras
puras init                  # scaffold a blank starter + puras.yaml
                            #   (--template hello-world scaffolds this guide's pack)
puras deploy                # zip the dir + push a deployment
puras run greeter --input name=Ada --input style=playful

puras.yaml is the pack manifest: it binds the directory to its remote skillpack (skillpack_id, slug) and carries the pack page's own title, description, and optional marketing block — see skill-yaml-reference for the full shape.

From here: cli-reference covers deploy / run / pull from your terminal, mcp-tools covers the same from an agent (fork_skillpack, pull_skillpack) plus running deployed skills, and sdk-client-reference covers calling them from application code. To go deeper on any field, the manifest spec is skill-yaml-reference; the agent's runtime tools are agent-tools-reference; the in-skill Python runtime is sdk-runtime-reference.

Once hello-world makes sense, the same four pieces — deterministic skills, an agent in the middle, custom tools, and subagents — scale up to any real pipeline.