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 handcrawl_siteinvoke
briefinfer
persistinvoke
the file, whole
conformance-gated upstream on every spec pushwebsite-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 contractsha256 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
cross-references
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 →