AGPL-3.0-or-later · forever.

the skeleton register

website-brief

Your intent sounds like: understand a site (domain · theme · assets) from a URL One of 10 skeletons the pack ships, green as-is under nika check. Fill the # SLOT: lines, repair from the fix lines, re-check. Machines read the catalog; the binary lists them with nika new '?'.

  • 11SLOT pointsthe only lines you edit
  • 4patterns lockedarrive correct, stay correct
  • 2tools grantedthe minimum for the job
  • 6504ccaesha-pinnedre-hashed on every test run

the plan

derived from the file, never drawn by hand
crawl_siteinvoke
briefinfer
persistinvoke

the file, whole

conformance-gated upstream on every spec push
website-brief.nika.yaml
# SPDX-License-Identifier: Apache-2.0# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json## TEMPLATE · website-brief · crawl a site → understand it → persist the brief.##   [crawl_site]────▶[brief]────▶[persist]#   nika:fetch        infer       nika:write## "Here is a URL, tell me what this company actually does." One bounded crawl,# one typed model pass, one JSON file downstream tools can read. The output is# a SHAPE, not prose — which is what makes it composable.## Demonstrates ·#   - `traverse:` · a bounded same-origin crawl from a single URL · one fetch#     task, N pages, no loop to write#   - `schema:` on `infer` · the brief is typed, so the next workflow can bind#     `.colors` without parsing anything#   - a JSON artifact written as a VALUE · `content:` takes one `${{ … }}`, never#     hand-typed braces## Needs · nothing to rehearse. For a REAL brief: network access and a live site#   in `const.site_url` — then delete the `on_error:` block so a dead host is loud.## Run · nika run <file> --model mock/echo    # rehearsal · green even with no network#   (on a networked machine the crawl really fetches const.site_url — it is permitted)nika: v1workflow:  id: website-brief-template    # SLOT: kebab-case workflow id  # SLOT: one honest sentence — `nika new` matches intents against these words  description: "crawl → brief → persist"# SLOT: provider/model · local · zero key. The schema below has five required# fields, so give it a seat that holds structure — `ollama/qwen2.5:14b` is# measured on this shape. `--model mock/echo` needs no seat at all.model: ollama/qwen2.5:14bconst:  site_url: "https://example.com"   # SLOT: the site to understand  out_path: "./out/brief.json"      # SLOT: where the brief landspermits:                            # the blast radius · default-deny once present  tools: ["nika:fetch", "nika:write"]  net: { http: ["example.com"] }    # SLOT: the host from const.site_url. ONE entry is                                    #   enough — `traverse:` is a same-origin BFS (+ the                                    #   robots probe), so the crawl never leaves this host.  fs: { write: ["./out/brief.json"] }   # SLOT: the one file · keep in step with const.out_path                                        #   (the infer step needs no grant — it is pure compute)tasks:  crawl_site:    invoke:      tool: "nika:fetch"      args:        url: "${{ const.site_url }}"        traverse: { max_pages: 5 }  # SLOT: crawl bound · 1..=25 (robots honored)    on_error:      # Offline rehearsal · a literal standing in for the crawl digest, so the      # brief step runs with no network at all. Delete once the site is real.      recover: "REHEARSAL DIGEST · Example Co · a small tool company · plain blue and white pages · buttons, a pricing table, one logo."  brief:    with:      crawl_site: ${{ tasks.crawl_site.output }}    infer:      max_tokens: 1200      # SLOT: the one model job · what should the brief capture? Keep slot      # markers out of the block below — it is prompt TEXT, not YAML, so a      # stray comment line is sent to the model verbatim.      prompt: |        From this site crawl, produce a creative brief: the domain of        activity, the dominant visual theme, the audience, the usable        colors and image assets.        Crawl digest · ${{ with.crawl_site }}      schema:                       # SLOT: the typed shape downstream tasks rely on        type: object        additionalProperties: false        properties:          domain: { type: string }          theme: { type: string }          audience: { type: string }          colors: { type: array, items: { type: string } }          assets: { type: array, items: { type: string } }        required: [domain, theme, audience, colors, assets]  persist:    with:      brief: ${{ tasks.brief.output }}    invoke:      tool: "nika:write"      args:        path: "${{ const.out_path }}"        create_dirs: true        # The path ends `.json`, so `content:` is ONE interpolation of a value        # the engine serializes. Typing `{ "domain": ${{ … }} }` by hand emits        # unquoted fields and the artifact stops being JSON.        content: "${{ with.brief }}"outputs:  brief: ${{ tasks.brief.output }}   # SLOT: the callable contract

sha256 6504ccae51b3a064. The copy above re-hashes to its pin on every test run (a copy is re-provable, never trusted). Source: website-brief.nika.yaml in the spec pack · open it in the playground →

the patterns it locks

  • fetch `traverse:` crawl
  • one typed infer
  • explicit persist
  • zero exec

Scaffold it locally: nika new website-brief my-flow.nika.yaml. Try the shape in the playground, or walk the showcase for the same patterns on real work. Read the spec →